ReasonReact context explained in action
Context
in react
is designed for sharing some global data between components located at different levels of the component tree. It allows to avoid passing props
all the way down to those components (“prop-drilling”) while still updating them whenever the value in context
changes.
Worth noting that it is recommended to use context
for low-frequent updates (quote by Sebastian Markbåge), due to a possible performance impact because of to the way react
finds subscribers to the context value. This topic would require its own article (or perhaps a book?), and I will not touch upon it here and instead focus on a practical example of using context
for seldom updates in a react
application with ReasonML
.
What are we building
We are going to build a feature with log in/log out, where we will put information about the user in context
, so that we can access it from anywhere in our app and customise it depending on whether the user is browsing anonymously or not. The source code in the article is in this repo, and a link to the mini-app with this feature is here.
There are many bits and pieces that have to be wired up together in order to get benefits and all the convenience react context provides, especially in a strongly-typed environment with ReasonML
, but it is definitely worth.
I will go through the steps needed to connect everything together and we will end up with a simple hook that allows to read the user data from context and dispatch and action to update it from any component, like this:
let (user, dispatch) = UserContext.useUser();
let handleLogIn = () => dispatch(UserLoggedIn(userName));
switch (user) {
| Anonymous => /** display login form */
| LoggedIn(userName) => /** say hi to the user! */
};
Scroll down to learn how 👇
Create provider and context
We will start with these steps:
- Create context,
- Create provider component,
- Create reusable hook to access context value.
We need to know whether the user using our app is anonymous or logged in, and what actions can change this, so let’s start with a few types:
/** Types.re */
type user =
| Anonymous
| LoggedIn(string);
type userAction =
| UserLoggedIn(string)
| UserLoggedOut;
LoggedIn
will hold user name, but can be any other type with more user data. We will use userAction
when implementing a reducer for our user state.
Now let’s create context and reusable hook to access the context value, in a file UserContext.re
:
/** initial value is Anonymous */
let context = React.createContext(Anonymous);
/** hook to easily access context value */
let useUser = () => React.useContext(context);
This is very similar to how you would do it in JS. Now let’s create context provider in a file UserProvider.re
/** UserProvider.re */
let make = React.Context.provider(UserContext.context);
/** Tell bucklescript how to translate props into JS */
let makeProps = (~value, ~children, ()) => {
"value": value,
"children": children,
};
What is that makeProps
for and why can’t we just create a normal component with [@react.component]
and make
? The question I asked myself many times until I got tired and dug into it and found out 🤦♀️🙃
Remember how we always have named arguments for props
in our reason
components, like ~id
or ~className
? JS doesn’t have such a feature, and all regular JS components just want to have props
as an object. So how does it compile to valid react
components in JS?
That’s what the attribute [@react.component]
is for. It will generate a function called makeProps
, that transforms those named arguments into a JS object to be used as props
in the JS compiled component.
React.Context.provider
already generates a react component, that uses props
as a JS object, but we want to use it as a reason
component with named args. That’s why we create makeProps
by hand and it will tell bucklescript how to translate our named args into a JS object, consumed as props
by the JS component. And in order to create an object that will compile cleanly to a JS object, we use bucklescript Object 2
bindings, that look like this:
{
"value": value,
"children": children,
}
So we are basically doing the job of [@react.component]
, but luckily it is not much, since the provider just needs a value and children 😅.
We can now use our provider component like <UserProvider...>
since we followed the convention
of having two functions make
and makeProps
in a file UserProvider
.
Update value in context
Now, we want to use our Provider
component and give it the user info, that we can update when the user logs in or logs out.
The important thing to understand here, is that if we want to update the value in context
and propagate the update to subscriber-components, the value needs to be on the state of some component. This component needs to render the provider component with the value from its own state.
Let’s call it Root
component:
/** Root.re */
type state = {user};
/** user and userAction defined in Types.re */
let reducer = (_, action) =>
switch (action) {
| UserLoggedIn(userName) => {user: LoggedIn(userName)}
| UserLoggedOut => {user: Anonymous}
};
[@react.component]
let make = () => {
let (state, dispatch) = React.useReducer(reducer, {user: Anonymous});
<UserProvider value=state.user>
<Page />
</UserProvider>;
};
Cool, now whenever the value in context changes, components using useUser
will be updated with the new value! Wait, the value actually never changes.. oh no! 😯
Let’s give our components possibility to update user data via context. We could pass the update function down as props
, which will be back to prop-drilling approach, but a more fun way is to include dispatch
in the context value itself.
Pass dispatch in context
Let’s pass our dispatch
along with user
as context value. Knowing that dispatch
accepts userAction
and returns unit
, we can modify the type of context value in UserContext.re
:
/** UserContext.re */
type dispatch = userAction => unit;
type contextValue = (user, dispatch);
let initValue: contextValue = (Anonymous, _ => ignore());
/** no changes when creating context */
and the root component:
/** Root.re */
let make = () => {
let (state, dispatch) = React.useReducer(reducer, {user: Anonymous});
<UserProvider value=(state.user, dispatch)>
<Page />
</UserProvider>;
}
Use context value via hook
And now the reward I promised in the beginning, an easy to use and convenient hook. I will just repeat it here once more, because it is cool:
let (user, dispatch) = UserContext.useUser();
let handleLogIn = () => dispatch(UserLoggedIn(userName));
switch (user) {
| Anonymous => /** display login form */
| LoggedIn(userName) => /** say hi to the user! */
};
Bonus: optimization techniques
Updating context value will cause subscribed components to re-render. In some cases we might want to avoid extra re-renders if we know they won’t bring any updates to our UI. For example, if a component only needs to update user via dispatch
, it won’t be interested in any updates to the actual user data, but it will still re-render if the user is updated.
This can be solved by having the dispatch
function in a separate context, which won’t update, since dispatch
is guaranteed to be stable. The other context will have the user data and will update the components that rely on it.
When the Root
component itself updates (if its props
are updated for example), it will recreate the tuple (user, dispatch)
passed in context and cause subscribed components to update. This can be solved by using useMemo
around the context value to make it stable.
We have now set up all we need to use context for storing and updating a small amount of global data in our reason-react
application. We have also looked into some underlying mechanisms of how context
works in react
and how components are compiled in reason-react
.
Have I missed something or made a mistake? Please let me know in the comments. Or just drop a line about how you are using context
in your application! 💬