Two Patterns with React Context
Posted on: 2021-08-27
For some reason, I am always copying a previous React Context before creating a new one. I recently realized that I was copying two different patterns. In this article, I'll expose the two patterns.
Class Pattern with React Hook
The first pattern is succinct but requires relying on a class. First, the logic of the shared code, the context code, is inside a class. It means that every value you want to share across your component via the context or any functions is stored in a class. So, three steps are required. The first one is to create a class.
export class MySharedLogic {
public message: string;
public setmessage(s){this.message=s;}
}
The second is to create the context.
export const MySharedLogicContext = createContext<MySharedLogic | undefined>(undefined);
This is to place the context in the React component tree. Usually, this is very high, next to the start of the application.
return <div>
<MySharedLogicContext.Provider value={instanceOfMySharedLogicClassHere}>
<div>
...
</div>
</MySharedLogicContext.Provider>
</div>;
From that point, any component under the context from the previous block of code can use the Read hook useContext to get access to everything from the class MySharedLogic.
const fromContext = useContext(MySharedLogicContext);
Object Pattern with React Hook
The second pattern is a little bit more verbose but avoid using a class. What we need to do is to define an interface with the contract of the shared logic.
export interface MySharedLogic {
message: string;
setMessage: (m: string) => void;
}
Then, we create the context with placeholder value or the actual value if we are able to provide it at this time. It can happen that the logic or information is only available at runtime, hence defined later. In the case that it is defined later, the code would look l like the following. To avoid having these empty functions, it is possible to allow the context to be MySharedLogic | undefined instead of only MySharedLogic.
export const MySharedLogicContext = createContext<MySharedLogic>({
message: "",
setMessage: () => {
/*Implementation later in a React Hook*/
}
});
The next step is to put the context in the React DOM tree. Similar to the previous pattern but at that time we can define the logic. To keep the code cleaner, I usually wrap the portion into a function so it looks like:
export interface MySharedLogicContextProviderProps {
children: ReactElement;
}
export const MySharedLogicContextProvider = (props: MySharedLogicContextProps): ReactElement => {
const [message, setMessage] = useState<string>("");
return (
<MySharedLogicContext.Provider
value={{
setMessage: (m: string) => {
setMessage(m);
},
message: message,
}}
>
{props.children}
</AppMessageContext.Provider>
);
};
The provider in the tree:
return <div>
<MySharedLogicContextProvider>
<div>
...
</div>
</MySharedLogicContextProvider>
</div>;
The Differences
The decision between the pattern is a matter of the role of the context. The class has less boilerplate to configure reducing the cognitive load to understand each part. However, if the role of the context is to share a user interface feature like having a global confirmation or error popup will get more complex. The class encapsulates the JavaScript/TypeScript code outside the lifecycle of React.
On the contrary, with more code without the class, the object pattern with the provider React Hook function gives you direct access to any user interface code. You might argue that with the class, it is possible to also access the user interface by creating a wrapper, similar to the object, instead of directly using the MySharedLogicContext.Provider. The observation is valid. However, the emphasis of these two patterns is that one relies on a class, the second not, and that without relying on a class, you must store the values of the context somewhere. In this article, I am storing the value inside a React state (useState). Another option could be to use store the values directly into the ECMAScript module where the createContext is called.
In either case, you have all the flexibility you desire and you will not be constrained. A heuristic I found is that if you need to have a context with reactive content, for example, a message that displays on the screen, the object pattern is ideal: because the data is stored in a state, the consumer with useContext will receive the new value and the one responsible to display the message in the user interface can react naturally. The class pattern is less prone to reactivity since the value will change but React will not render a change. Hence, I found using the class pattern in scenarios where I am pushing changes. For example, a context that checks the user's permission where the context was filled at the beginning of the application's boot process and then kept the value in the context to be consumed as needed.