It's been almost 7 years since the first version of React was released, and it is still one of the most popular libraries at the moment you want to work regarding frontend. The switch into JSX syntactic sugar, suspend, hooks, memo ( or the introduction of the pure component), all of them have been involved since that first released version of 23th May of 2013. One of the great points of React is its way to manage a state or pass state through different components, and definitely, that one has been evolving as well. There are different ways to tackle the state in a React application, and that's what I would like to explain in this post. I will separate it in 2 parts: 1) What is React context 2) Which option to pick as state management.
What Is React Context
One technique that I like currently is to use the native React context solution for state management. That I like this idea does not mean that I am just picking this one as a unique option in my day a day, I will explain it later.
From React docs:
Context provides a way to pass data through the component tree without
having to pass props down manually at every level.
It's as easy as it sounds: Pass data through components, avoiding the prop drilling. Some people consider prop drilling as an anti-pattern. I always think: that decision depends on the context (hopefully you got this bad joke, hehe). Jokes apart, I do not think of it as an anti-pattern. It seriously depends on the context, like the size of your app, how much scalable do you need it, how maintainable you want to do it, or is just going to be a one time coded app that rarely is going to be used? Here it's a good explanation from Kent C. Dodds regarding Prop drilling.
Let's assume we are working on an auth app, and we do not want to pass data through. We decided that our bundle is quite important, and we want to keep it as light as possible, so no external library to control it, and this is our code
// User.js
import React from 'react'
function User(){
return (
<React.Fragment> Hi {user.firstName} </React.Fragment>
)
}
This is our goal, now the question is: how do we get this user.firstName
. Let's create our first React context for it. Keep in mind that if you are working with classes, it can be slightly different syntax. for that, check React docs.
Creating Context
Let's introduce the first picture of our Context and split it by parts
// UserContext.js
import React from 'react'
export const UserContext = React.createContext(undefined)
export function UserProvider() {
const [user, setUser] = React.useState(undefined)
const manageUser = {user, setUser}
return (
<UserContext.Provider value={manageUser}>
{ children }
</UserContext.Provider>
)
}
Let's break this code into pieces:
-
const UserContext = React.createContext(undefined)
.- needed for the creation of the context itself. You can pass a first value into the context. In this case I set undefined
-
const [user, setUser] = React.useState(undefined)
- Single hook for the user check hooks if you are not familiarized with it.
-
const manageUser = {user, setUser}
- For learning purposes, I show explicitly the object of the user. That could go directly into the provider
<UserContext.Provider value={manageUser}>
- The context we previously set, now it's passed as a React component, notice that needs to be under
.Provider
to make it work. it accepts thevalue
option, which is exactly the value you are going to pass to the children.
- For learning purposes, I show explicitly the object of the user. That could go directly into the provider
With that, you have a context created. Not so complicated at the end :)
Consuming context
Coming back to our original skeleton, now we can update it to make it working:
// User.js
import React from 'react'
import { UserContext, UserProvider } from './UserContext.js'
import { fetchUser } from './utils' // Let's pretend we got a method for fetching the info of the user
function UserWithContext(){
const {user, setUser} = React.useContext(UserContext)
React.useEffect(() => {
const infoUser = fetchUser()
setUser(infoUser)
}, [])
if (!user || !user.firstName) {
return <React.Fragment> You are not logged in</React.Fragment>
}
return (
<React.Fragment> Hi {user.firstName} </React.Fragment>
)
}
function User(){
return (
<UserProvider>
<UserWithContext />
</UserProvider>
)
}
Voila, now we can retrieve the user info or set the user easily from context. Notice how I renamed the component into UserWithContext, and the User component itself is returning the provider as a Wrapper. That is important in order to consume the context UserContext
. Otherwise, it would return undefined
as value.
Why did I rename the main one instead of the 'Wrapper'? Because when you import, you can go for User. I think to see <UserWithProvider />
it's not the best, because you are letting know to the user who consumens this component, that you have some wrappers there.Instead, I would expect tu plug it and do not know how is build internally, like<User />
.
This example has been created for learning purposes you do not need all of this if you just need a single component to consume all this info. Imagine the scenario of
<User>
<ManageAccount>
<OverviewInfo />
<ManageInfo />
...
</ManageAccount>
</User>
Here you have deeper levels, and you need this info user. Here it comes the value, when it contains a children that has another children and so on...
Best Practices
As you notice, for consuming the context I need to import the UserContext, UserProvider
and even user the React.useContext
. It's not a problem but we always have to remember the useContext and import the UserContext itself, so they are always coupled together. We can simplify this process if we move everything into a custom hook. We could do the following
// UserContext.js
import React from 'react'
const UserContext = React.createContext(undefined)
export function UserProvider() {
const [user, setUser] = React.useState({})
const manageUser = {user, setUser}
return (
<UserContext.Provider value={manageUser}>
{ children }
</UserContext.Provider>
)
}
export function useUser() {
const {user, setUser} = React.useContext(UserContext)
if( !user || !setUser) {
throw new Error ('Provider of User has no value')
}
return { user, setUser}
}
With this technique we get 2 benefits:
- Now the components who consumes this hook does not have to know that it's necessary to wrap the
useContext
, so we decoupled and hide this abstraction. They can just get the benefit of our user by just doing auseUser()
without knowing what is internally - We throw an error in case there no info on the user or the method setUser. Notice how I slightly modified the initial value of the
useState
, otherwise, since hooks are async, it would always trigger this error because of the initial undefined (it's up to you to decide what to show for the initial value). The benefit of throwing this error is pure agile: fail early and fix it quick. In my opinion, it's better to just fail here and know that something is not going on well than just passing not valid info and fail at some point in deeper components.
Another refactor we can do here:
// UserContext.js
import React from 'react'
const UserContext = React.createContext(undefined)
const SetUserContext = React.createContext(undefined)
export function UserProvider() {
const [user, setUser] = React.useState({})
return (
<SetUserContext.Provider value={setUser}>
<UserContext.Provider value={user}>
{ children }
</UserContext.Provider>
</SetUserContext.Provider>
)
}
export function useUser() {
const { user } = React.useContext(UserContext)
if( !user ) {
throw new Error ('Provider of User has no value')
}
return user
}
export function useSetUser() {
const { setUser } = React.useContext(SetUserContext)
if(!setUser) {
throw new Error ('Provider of SetUser has no value')
}
return setUser
}
Notice that I have 2 contexts now with 2 custom hooks, and I use one for each of the user properties coming from useState ( the object, and the method). Why would I do that?
Think about this, every time this component is re-rendered, a new object identity
will be created for both user and setUser at the moment it is sent to the children, causing a re-render on all children's components. That, in big applications, will cause performance issues. Mainly every re-render it's generating a new const manageUser = {user, setUser}
otherwise. Remember that in javascript {} === {} // false
comparing the same object with same properties will be detected as different objects, and here is the tricky part: because of {user, setUser} === {user, setUser} // false
this will re-generate always a new const manageUser
and will re-render all children. React is doing a deep comparison with objects if they are inside the useState
.
Probably you are a bit confused here. The previous paragraph was just some theory, here the practical side: Adding 2 contexts and hooks, one per each, will solve the re-render problem. You isolate them. The first Provider, the method, is just that: a method. So it's quite unlikely that it will ever change. The second Provider, the user object, is more likely to be changed, and that's why it goes in the second position: It will trigger a re-render on the children for the user, but never a re-render because of the method. If the position would be
<UserContext.Provider value={user}>
<SetUserContext.Provider value={setUser}>
{ children }
</SetUserContext.Provider>
</UserContext.Provider>
then every time the user is changed, it would be triggered setUser as well. Since that's a method, and we are not interested in re-render just a method that will never change, we put 'static' content in the top and the content that it's about to be changed more close to the children
I will talk in the future regarding object equality and probably this topic can be clearer because it's not easy to understand it.
My last refactor lies in the following
Context, Props drilling, Redux, Mobx, Xstate, Apollo Link... What to use?
There are several options to manage your state. I just presented one that I like, but that doesn't mean it needs to be the one and unique to follow. Every library or technique has its pros and cons, and it's up to you to decide at which moment you need one.
Let's cluster them from global state vs local state
Global state
So you configure at the very top of your app, probably <App />
component, this library you want to implement, so you can share info with all the components.
In my eyes, this can be a valid solution when you need to share some critical info with all the components ( maybe the user we talked before needs to be recognized in all components and it would be the better host as a global state). I assume you pick one solution ( it's on you to have Xstate and mobx and redux in your app, but it's hard to know where to pick the info for every library).
I would never use this solution in case I need to share the info to just 2 or 3 components that are going nested. Or even if they are not, consider the price to have a helper function that provides you this info VS having it in a global state from the first instance.
Clear example. I fetch info in one component, add a loading
state in redux ( for example) to check in the nested component if it's loading. In case it is, I will show a spinner. This scenario, in my opinion, is a no go for a global solution. The complexity you had to add to just know if it's loading or not, wasn't paid off.
If you have an App that has I18N and depending on the language, you are fetching info from some headless CMS, and the language determines one endpoint or another, then I see it as a valid solution. So a lot of components need to know the language. Therefore, they can fetch with one token or another to the headless cms provider for dynamic info.
Local state
I would cluster local state as mainly native solutions from React as prop drilling or Context ( if there are some libraries based on local state management, let me know because I have no idea if there's any).
Prop drilling is great, you can go far just with that. It's the simplest and straightforward solution. If I have a component that is just nesting 2 or 3 more components, definitely I would go for this solution. One example: the container/component solution: I used to put the business logic in the container, and move the rendering and methods to help to render into the component. I would never think about something different than prop drilling approach. Just pass info from one component to another.
React context is great in the scenario that you have multiples components, all of them somehow connected into the same parent component. That can be dramatically misused as a global state, so please keep in mind you can have at the end as many contexts as possible in your app, try always to encapsulate them and make them as smaller as possible for your use cases. One example: I've been working in a Carousel at work. I have all the business logic in a container, then. I move into component, and the component has the content of the current slide to be shown, but as well some buttons for navigation, and arrows for navigation. We are easily talking around 6 components, all of them connected by the Carousel Container. I give as a valid here the cost to create a context
Last comments to keep in mind
Every solution always comes with a cost of effort and time. Cheapest solutions are easy to implement but not scalable and maintainable. Expensive solutions are scalable, can be hard to maintain as well, and needs a lot of time and effort. And here is my most important message: Always think for the use case you need what's the best approach in relation effort/cost. Pick the one that can work better for you at that specific moment and just that specific moment. Don't try to guess the future of that file in 6 months, because you'll never know.
Things I keep in mind to choose an approach:
- Small apps don't need global state( they can have it, but there's a high chance of possibilities that you can live without)
- If your app is big, it will help to have some global state.
- How many people are working on this project? If it's just you, it's not a necessary global state. However, if there's an app with 20 people working under different domains ( imagine an E-shop: Checkout VS My Account), then probably you need to share some global info
- How much needs to be scalable the component you need to work. If you know that's going to be a small component, stick with prop drilling
- How much maintainability does it need? Try to remember that maybe you come into that file after 1 year that no one touched it. How can you make your life easy and be able to understand what's going on in the easiest and fastest way possible.
I hope this post helps you to understand the Context and which approach to take. If you want to talk with me, let's connect on twitter.
See the original post at my blog suetBabySuet
Top comments (0)