Simple ways to manage state in React - Client state
July 9th, 2022 - 13 minute read
TL:DR React is a state management library.
Hear me out.
Managing state is one of the most difficult things to do in React. State is data, usually data that can change. There are a plethora of options and opinions. One of the reasons for this is the failure to separate concerns, or better still, state type.
Most applications you will ever build will have 2 types of state: `client/application state`
; and `server cache.`
The first step in managing state with React is knowing the difference between these 2 types of state and where and how to manage them.
Client state refers to state that lives within your application, that is, it’s only useful in the UI for controlling interactive parts (like modal `isOpenstate`
, an active nav link, form input value).
Server cache, on the other hand, is state that's actually stored on the server and we request it in the client for quick access (like user data). Think of it this way: A list of articles that you fetch, the details of a User you want to display, etc), your app does not own it. We have only borrowed it to display the most recent version of it on the screen for the user. It is the server that owns the data.
Server cache is not the same as UI state and should be handled differently.
React has made state management simple (not easy, but plain and basic enough to understand ). Let’s talk about the client state first and ways to manage it.
Client state
Client state is simply state managed inherently in the UI. It can be further separated into local client state and global client state.
Local state is UI state that is used in a single or few components that are `colocated`
. State colocation simply means to state as close to where it's relevant as possible. See this article by Kent C Dodds. Dan Abramov said something similar: "Things that change together should be located as close as reasonable.”
Colocation can improve the overall maintenance of our application.
Let’s look at an example of `colocating`
and `lifting state.`
js
At the moment, we’re only rendering the name of the person in the `app`
component. What if we need to access the `food`
prop in the `app`
component?
How are we going to get access to the `food`
if our state is living in the `Bestfood`
component? Both `Bestfood`
and `Display`
components are sibling components so the `BestFood`
component cannot pass the food to the Display component.
The solution is to do the same thing we’re currently doing for the `Name`
component; which is "lift the `food`
state" from the `BestFood`
component to the least common parent, the `App`
component, and then pass the `food`
state and the mechanism for updating that state as props to the component that needs it.
js
Once you learn how to do this, it becomes second nature. But one thing we’re not quite as good as is pushing state back down, or `colocating`
state.
Let’s say our `Display`
use case gets changed and we no longer need to pass the food down.
Often, people will actually just leave it right there without making any other changes. But we need to remember that we no longer need this `food`
prop on the `Display`
where we’re rendering it.
And because the `food`
state is only being used by a single component, we can move it back to that component to get it colocated.
js
And with that we’ve colocated our state, making the app more performant and easier to maintain in the long run.
The truth is there are only a few states that are considered truly global, e.g. `theme`
(light or dark mode), every other local state should either be stored in the function where it is called or colocated in the nearest common parent.
Prop Drilling
When building large applications, passing state can get a little complicated when you have many layers of components and you need to pass some piece of state deeply through the tree. The nearest common parent could be far removed from the components that need data, and lifting state up that high can lead to a situation sometimes called `prop drilling.`
A simple way to look at prop drilling is that a parent creates a state, passes it down to “middlemen”; components that do not require the state, and then down the tree eventually to the component(s) that needs it.
In other words, a component receives a state or props just so that another component below it can access that.
Most react devs avoid it and would reach for libraries such as `redux`
or React context the moment they encounter this problem.
Don't be afraid of prop drilling
Let’s look at an example of prop drilling in action.
js
Header
Please login
Footer
Here, we have this Dashboard layout where the `WelcomeMessage`
is displayed based on the current user's name. To get the value, we would need to prop drill the `user`
object three levels deep until it gets to the `WelcomeMessage`
component.
This is a contrived example, and prop drilling may not be a big deal in such a scenario and in many instances for that matter, but it can be an annoyance in really complex state management architectures.
When you truly encounter the latter and prop drilling may potentially complicate your state management, then there are a couple of other solutions we can use to avoid prop drilling. The first is `component composition`
.
Component Composition
With composition, you basically make a prop that is the react element that you want to render in a specific place.
Let's use the same prop drilling example and see how we can use composition instead.
js
We have eliminated one level of prop drilling. We no longer pass the `DashboardNav`
and `DashboardContent`
inside the Dashboard, instead, we pass it the special `children`
prop and pass the `user`
directly to the `DashboardContent`
.
Let's take it a step further and do the same thing for the Dashboard content.
js
And like that the Dashboard is more composable and customizable. And we're no longer prop drilling.
Our app works like before too.
Header
Please login
Footer
Sometimes you may need multiple “holes” in a component. Another way React composes is to make a component that is a wrapper for another component. In such cases, you may come up with your own convention instead of using the `children`
prop.
Let's see it in action with this contrived chat app.
js
What we have simply done is pass arbitrary values as props to the `ChatApp`
component.
React elements like `Contacts`
and `Chat`
are just objects, so they can be passed as props like any other data. This is a very common use case for composition.
The next solution is the React context API.
React Context
Context is useful when you have some value that you want to make accessible to a portion of your React component tree, without passing that value down as props through each level of components.
We'll use react context to simplify the state management of our previous Dashboard app.
What we simply need to do is create a context using the `createContext`
API, and wrap our `App`
component with the context provider.
We also removed the user object from the `Dashboard`
, `DashboardContent`
, and `WelcomeMessage`
components.
Lastly, we'll destructure the `currentUser`
object using the useContext API and use the `user`
directly inside the `WelcomeMessage`
component.
Here's what the implementation looks like.
js
And you guessed it, our app works still! 😉
Header
Please login
Footer
Conclusion
Essentially, with client/ui state, start with just react. Most apps barely have any global app state after server state is handled. If you truly still need a global state in your app, try zustand or jotai. And for anything more complex, try xstate.
This was meant to be one really long blog post, but I decided to break it into 2 parts. Read the second part here where I talk about managing server state.