It's 2020 and React is still the most popular frontend framework in the world. It's not just because it's relatively simpler. The fact that it keeps getting better is what has keeping me hooked (unintentional pun). The introduction of hooks changed the ecosystem from class based components to functions and made writing React way more fun. But there hasn't been a particular state management tool that is the go to option in React.
Redux is really popular. But a major source of complaint with Redux is how difficult it is learn at the beginning due to a lot of boilerplate. Recently I got to see some tweets
The impact React Query is having on some Redux users is mind boggling to me.
I'm seeing it more every day that someone starts integrating the two and ultimately ends up with such a small amount of redux state that they drop Redux altogether and put it in React context.
🤯22:34 PM - 20 Apr 2020
This led me to go an learning spree and I got to know some exciting patterns and packages which might completely change how you view hooks and global state in general(it did for me).
When I first thought I would write this article series I had way too many options for a title. There was State Management 2020, Custom Hooks in React, and a few others. But finally I decided to go with Ciao Redux(Goodbye Redux), since that seemed like the end goal for this article series.
This article is inspired by this great talk from Tanner Linsley at JSConf Hawaii 2020. I recommend you to watch it if you haven't already.
So let's get started.
How do you see State in React?
One would simply say, State is all the data present in frontend or it's what you fetch from the server. But when you have used React for building applications for a few time now, you would understand the point I'm going to make.
@jhooks All apps have two types of state: UI State and Server Cache. Put all your server cache in react-query and the rest of your state is pretty simply managed within React state/context.04:57 AM - 22 Apr 2020
State can be majorly divided into 2 types:
- UI State
- Server Cache
You maybe wondering WTH I'm talking about. Let me explain.
UI State is the state or information for managing your UI. For example, Dark/Light theme, toggle a dropdown, manage some error state in forms. Server Cache is the data you receive from the server like a user details, list of products etc.
Managing State
Lets start with basics. And build something for example's sake while we are at it. No, not a todo list. We have enough tutorials for that already. We are gonna build a simple application with a login screen and a home screen.
useState
The useState
hook allows us to use state inside a functional component. So bye bye all the hassles of declaring state in constructor, accessing it through this
. One can simply do
import { useState } from 'react'
const [name, setName] = useState("")
and we get name
variable and a function to update the variable as setName
.
Now let's use this knowledge to make a login form for our page.
import React, { useState } from 'react'
export default function Login() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [emailError, setEmailError] = useState(false)
const [passwordError, setPasswordError] = useState(false)
const [isLoading, setIsLoading] = useState(false)
async function handleSubmit() {
setIsLoading(true)
const res = await axios.post(url, {email, password})
if(res.data.status === "EMAIL_ERROR") {
setEmailError(true)
}
if(res.data.status === "PASSWORD_ERROR") {
setPasswordError(true)
}
// Otherwise do stuff
}
return (
<div>
<input
type="text"
value={email}
onChange={
e => setEmail(e.target.value)
}
/>
{emailError && <ErrorComponent type="EMAIL_ERROR" />}
<input
type="password"
value={password}
onChange={
e => setPassword(e.target.value)
}
/>
{passwordError && <ErrorComponent type="PASSWORD_ERROR" />}
{ isLoading
? <button onClick={() => handleSubmit()}>Sign in</button>
: <LoadingButton /> }
</div>
)
}
This works. But this must not be the best way is it. And this can pretty easily go out of hand with addition of few other factors or validation checks for example.
useReducer
People familiar with Redux must know useReducer
works just like Redux does. For those who don't here's how it works.
Action -------> Dispatch -------> Reducer --------> Store
You create an action and dispatch it which goes through the reducer and updates the store. Let's implement it in the previous example and see how it works.
import React, { useReducer } from 'react'
const initialState = {
user: {
email: "",
password: ""
},
errors: {
email: false,
password: false
},
isLoading: false
}
const reducer = (state, action) => {
switch (action.type) {
case 'CHANGE_VALUE':
return {
...state,
user: {
...state.user,
[action.field]: action.data
}
}
case 'ERROR':
return {
...state,
errors: {
...state.errors,
[action.type]: true
}
}
case 'LOADING':
return {
...state,
isLoading: true
}
default:
return state
}
}
export default function Login() {
const [state, dispatch] = useReducer(reducer, initialState)
async function handleSubmit() {
dispatch({type: 'LOADING'})
const res = await axios.post(url, store.user)
if(res.data.status === "EMAIL_ERROR") {
dispatch({type: 'ERROR', field: "email"})
}
if(res.data.status === "PASSWORD_ERROR") {
dispatch({type: 'ERROR', field: "password"})
}
// Otherwise do stuff
}
return (
<div>
<input
type="text"
value={state.user.email}
onChange={
e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "email"})
}
/>
{state.errors.email && <ErrorComponent type="EMAIL_ERROR" />}
<input
type="password"
onChange={
value={state.user.password}
e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "password"})
}
/>
{state.errors.password && <ErrorComponent type="PASSWORD_ERROR" />}
<button onClick={() => handleSubmit()}>Sign in</button>
</div>
)
}
This looks good, we don't deal with separate functions, we declare one reducer and define some actions and corresponding store changes. This is quite helpful because while using useState
, we can easily lose track of the number of variables as our requirement grows. You must have a noticed this is much longer than the previous code, which takes us to the next section.
Abstracting logic from UI
While developing an application in react you should always try to keep your business logic away from your UI code. The UI component, which interacts with the user should only know what interactions the user can do(actions). Plus this provides proper structure as well good maintainability to your codebase. This was well supported by redux in which we can define our actions elsewhere which would take care of all the logic, keeping our UI code clean. But how do we achieve that with hooks. Custom hooks to the rescue!
Custom Hooks
React allows you to create your own custom hooks for better separation and sharing of logic across components. For the above example, we can create a file called hooks/useLoginReducer.js
import { useReducer } from 'react'
const initialState = {
user: {
email: "",
password: ""
},
errors: {
email: false,
password: false
},
isLoading: false
}
const reducer = (state, action) => {
switch (action.type) {
case 'CHANGE_VALUE':
return {
...state,
user: {
...state.user,
[action.field]: action.data
}
}
case 'ERROR':
return {
...state,
errors: {
...state.errors,
[action.type]: true
}
}
case 'LOADING':
return {
...state,
isLoading: true
}
default:
return state
}
}
export default function useLoginReducer() {
const [store, dispatch] = useReducer(reducer, initialState)
return [store, dispatch]
}
Then in the Login component
import React from 'react'
import useLoginReducer from '../hooks/useLoginReducer'
export default function Login() {
const [store, dispatch] = useLoginReducer()
...
}
Voila! We separated the logic from the component and it looks so much cleaner now. Custom hooks can be used as such to a great effect for separation of concerns.
Let's go ahead to the best part.
Global State
Managing global state is what 3rd party libraries like Redux aim to provide, because prop drilling is hell. React has Context API, which allows to pass data between components. Context allows you declare a Provider
which stores or initialises the data and Consumer
which can read or update the data. It is used by Redux in the background, but
- it was unstable for a lot of time
- needed render props which led to less readability
With the introduction of React hooks however, using context became a lot more easier. One can easily declare a global state and use them by combining hooks
and context
. Let's take a look at an example we used above. Suppose after login you want update the global store with user's details which can be used in a Navbar component to display the user's name.
We declare a context first and use hooks to store and update data.
const globalContext = React.createContext()
const intialState = {
user: {
...
}
}
const reducer = {
...
}
export const StoreProvider = ({children}) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
//memoizes the contextValue so only rerenders if store or dispatch change
const contextValue = React.useMemo(
() => [store, dispatch],
[store, dispatch]
)
return (
<globalContext.Provider value={contextValue}>
{children}
</globalContext.Provider>
)
}
export function useStore() {
return React.useContext(globalContext)
}
So let me explain through the code here. We first create a context. Then we are using useReducer inside a component to create the store and dispatch method. We are using useMemo
to create a context variable to update only when one of it's dependencies change. Then we are returning the context.Provider
component with value as the context variable. In the last part we are using the useContext
hook which simply allows us to use the context inside a functional component provided it lies inside the Provider
.
// App.js
import React from 'react';
import { StoreProvider, useStore } from './context';
function App() {
return (
<StoreProvider>
<Navbar />
...
</StoreProvider>
);
}
// Login.js
import React from 'react';
import { useStore } from './context'
function Login() {
const [, dispatch] = useStore()
...
function handleSubmit() {
...
dispatch(...)
}
}
// Navbar.js
import React from 'react';
import { useStore } from './context';
function Navbar() {
const [{user}, dispatch] = useStore()
return (
...
<li>{user.name}</li>
)
}
So we wrap the app component in the StoreProvider
and use the useStore
function we returned to access the store value and dispatch function at a nested component. Sounds awesome right. Umm not so much. There are a lot of issues in this. Let's take a look.
- Firstly, since we are exporting both
store
anddispatch
. Any component which updates the component (uses dispatch only) and doesn't use the store will also rerender everytime the state changes. This is because a new data object is formed everytime context value changes. This is undesirable. - Secondly, we are using a single store for all our components. When we would add any other state to the reducer initialState, things will grow a lot. Plus every component that consumes the context will rerender everytime the state changes. This is undesirable and can break your application.
So what can we do to solve these. A few days I came across this tweet thread
@tannerlinsley @kentcdodds Most projects don’t need Redux if they use React Context correctly
So many problems b/c people try to put all their state in one context
If you break them up by concern instead, use useReducer, separate get/set contexts as needed, useMemo as needed -> React Context is golden14:47 PM - 21 Apr 2020
@dibfirman 1. Context + useState
2. Context + useReducer
3. DispatchContext + StateContext + useReducer
4. Multiple Providers of #3 for state "slices"
At any stage: Profile for slow renders, then useMemo
With those 4 stages and useMemo, I believe you can solve 99% of perf challenges.22:58 PM - 20 Apr 2020
Problem solved. This is what we needed. Now's let's implement that and I'll explain it along with.
For the first problem, we can simply separate the store and dispatch into to different contexts DispatchContext
for updating the store and StoreContext
for using the store.
const storeContext = React.createContext()
const dispatchContext = React.createContext()
const intialState = {
user: {
...
}
}
const reducer = {
...
}
export const StoreProvider = ({children}) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
return (
<dispatchContext.Provider value={dispatch}>
<storeContext.Provider value={store}>
{children}
</storeContext.Provider>
</dispatchContext.Provider>
)
}
export function useStore() {
return React.useContext(storeContext)
}
export function useDispatch() {
return React.useContext(dispatchContext)
}
Then simply we can only import useDispatch
or useStore
according to our case.
// App.js
import React from 'react';
import { StoreProvider } from './context';
function App() {
return (
<StoreProvider>
<Navbar />
...
</StoreProvider>
);
}
//Login.js
import React from 'react';
import { useDispatch } from './context'
function Login() {
const dispatch = useDispatch()
...
function handleSubmit() {
...
dispatch(...)
}
}
// Navbar.js
import React from 'react';
import { useStore } from './context'
function Navbar() {
const {user} = useStore()
return (
...
<li>{user.name}</li>
)
}
Now moving on to the second problem. It's really simple, we don't need to create a single store. I had difficulty using context previously primarily due to this reason only. Even in Redux, we separate reducers and combine them.
We can simply define a function which takes in initialState
and reducer
and returns a store. Let's see how it's done.
import React from 'react'
export default function makeStore(reducer, initialState) {
const storeContext = React.createContext()
const dispatchContext = React.createContext()
const StoreProvider = ({children}) => {
const [store, dispatch] = React.useReducer(reducer, initialState)
return (
<dispatchContext.Provider value={dispatch}>
<storeContext.Provider value={store}>
{children}
</storeContext.Provider>
</dispatchContext.Provider>
)
}
function useStore() {
return React.useContext(storeContext)
}
function useDispatch() {
return React.useContext(dispatchContext)
}
return [StoreProvider, useStore, useDispatch]
}
Then we can declare our userContext
as follows.
import makeStore from '../store'
const initalState = {
user: {
...
}
}
const reducer = (state, action) => {
switch (action.type) {
...
...
}
}
const [
UserProvider,
useUserStore,
useUserDispatch
] = makeStore(reducer, initalState)
export { UserProvider, useUserStore, useUserDispatch }
And finally use it when we need
// App.js
import React from 'react';
import { UserProvider } from './userStoreContext';
function App() {
return (
<UserProvider>
<Navbar />
...
</UserProvider>
);
}
// Login.js
import React from 'react';
import { useUserDispatch } from './userStoreContext'
function Login() {
const dispatch = useUserDispatch()
...
function handleSubmit() {
...
dispatch(...)
}
}
// Navbar.js
import React from 'react';
import { useUserStore } from './userStoreContext'
function Navbar() {
const {user} = useUserStore()
return (
...
<li>{user.name}</li>
)
}
Done. If we want another store we can simply make another store and wrap it around our app or the components where you want to use it. For example
function App() {
return (
<UserProvider>
<Navbar />
<ProductProvider>
<Products />
</ProductProvider>
</UserProvider>
);
}
Whooh. This was it for the first part of the series. Hope you have learned how to use hooks and context effectively. In the next articles I'm going to talk about react-query
and how to deal with server cache. Stay tuned.
Top comments (19)
So let's say I have 3 reducers (providers) and 2 UI components, I have to wrap every single component for 3 times (3 providers) if I want to use all stores in these components?
What happens if I wrap both components together? I mean is that the right way to do it? Are there any performance issues or something?
ROFL as I was writing this I actually figure out how great this is, I still wanna post this tho 😅😅, let's assume I only want to use providers 1 and 2 in comp1, I just have to move Component1 by 1 line up, there is no need to wrap it in provider3 if it won't use any features from it.
This provider actually acts like that connect function from redux right?
Great article BTW, I didn't really have time to test hooks until now, and I am glad I did now, and run into this post it was really helpful!!
Hey Mario,
Sorry for the late reply. You don't have to wrap all the components separately. Also yes there's no need to wrap the 3rd component. The provider is similar to connect function, in Redux we generally combine the reducers.
Hey Ankit,
Thanks for your response to the above question by Mario. What is the best way to wrap the without incurring performance issues? Thanks
Thanks for the article, it's great. But FYI, the word 'ciao' is in Italian, and it means both 'hello' as well as 'goodbye'. On the other hand, the word 'chau', is in Spanish, and it means only 'goodbye', which would be more appropriate in this context :)
Ah, thanks for the info mate. Today years old. Always thought ciao meant "Goodbye". Will update it.
You were almost right, in Spanish it does. Chau = Adios = Goodbye. Chau more coloquial, adios slightly more formal...both used extensively.
Thank you for the great article. Cheers.
Glad it helped. PS. For some reason "Chau" did not sound good in my head. So went with Adios :D
That was nice. Actually my question is I have tried to learn redux and found it tough as you have mentioned above. I was planning to give it one more shot. In your opinion should I completely ditch it or continue it and learn it on a fundamental level to at least have an idea.
The
useReducer
hook is actually a simpler way to understand Redux. If you understand that then you should understand Redux as well. Learning the fundamentals will help because this pattern is used a lot. Here's a good piece you can readcode-cartoons.com/a-cartoon-intro-...
Thanks
I started a global state using hooks and I came to the same solution! haha thanks for sharing, the only missing step was React.useMemo, very interesting! ❤️
Ankit, what do you think creating a npm package for this?
I think there's a package called
unstated-next
which is really good for sharing custom hooksThis article seems to be underrated, amigo :)
Thank you!
You are welcome. :D
Awesome write-up! Really well explained. Looking forward to the react-query one as well as it's also something I'm figuring out how to use!
Thank you Ankit, will be sure to try this workflow!
Thanks! This really helped 🙂
Awesome.. this post help me a lot..! thanks