Every application needs some kind of state management. Let’s start with the most basic one, and we will see how things change with scale.
2.1 Creating a basic global store
The idea here is to have a useState
that will store our state and update it, and then we will use react context to pass it down to components.
So now we will create a new context named StoreContext
and in its value the first item will be the store itself and the second item will be setStore so that we can update it.
import React, { createContext, useContext, useMemo, useState } from 'react'
const StoreContext = createContext()
export const StoreProvider = ({ children, initialState }) => {
const [store, setStore] = useState(initialState)
const contextValue = useMemo(() => [store, setStore], [store])
return (
<StoreContext.Provider value={contextValue}>
{children}
</StoreContext.Provider>
)
}
export const useStore = () => {
return useContext(StoreContext)
}
export default StoreContext
2.2 Some things don’t seem right
There’s only so much growing your store can do with useState
and at one point it will become a PIA to update your store using setStore. So let’s add a useReducer
in here and now our code looks something like,
import React, { createContext, useContext, useMemo, useReducer } from 'react'
const StoreContext = createContext()
export const StoreProvider = ({ children, initialState, reducer }) => {
const [store, dispatch] = useReducer(reducer, initialState)
const contextValue = useMemo(() => [store, dispatch], [store])
return (
<StoreContext.Provider value={contextValue}>
{children}
</StoreContext.Provider>
)
}
export const useStore = () => {
return useContext(StoreContext)
}
export default StoreContext
The problem with context is whenever it changes the whole tree under it re-renders, and that can be a huge performance issue. So even if we are just dispatching an action, our component will re-render. Now to fix that, let’s create a different context for storing the dispatch function, and we will use it with a useDispatch
hook.
import React, { createContext, useContext, useReducer } from 'react'
const StoreContext = createContext()
export const DispatchContext = createContext()
export const StoreProvider = ({ initialState, reducer, children }) => {
const [store, dispatch] = useReducer(reducer, initialState)
return (
<DispatchContext.Provider value={dispatch}>
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
</DispatchContext.Provider>
)
}
export const useStore = () => {
return useContext(StoreContext)
}
export const useDispatch = () => {
return useContext(DispatchContext)
}
export default StoreContext
And how we use this is by wrapping our App
first in DispatchContext
and then StoreContext
and then in our component
import React, { useRef } from 'react'
import { useDispatch, useStore } from '@state/context-reducer'
const Example = () => {
const dispatch = useDispatch()
const store = useStore()
return (
<div className="my-3">
<p>{JSON.stringify(store)}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Dispatch</button>
</div>
)
}
export default Example
2.3 One step further
So, only one global state? You might be wondering.
Rolls up my sleeves And here is where generator function comes in. Basically, we can make a function makeStore
that takes in the reducer and initialState, and gives us a provider, a useStore and a useDispatch, so that we can easily make multiple stores.
import React, { createContext, useContext, useReducer } from 'react'
export default function makeStore(reducer, initialState) {
const StoreContext = createContext(null)
const DispatchContext = createContext(null)
const StoreProvider = ({ children }) => {
const [store, dispatch] = useReducer(reducer, initialState)
return (
<DispatchContext.Provider value={dispatch}>
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
</DispatchContext.Provider>
)
}
const useStore = () => {
return useContext(StoreContext)
}
const useDispatch = () => {
return useContext(DispatchContext)
}
return [StoreProvider, useStore, useDispatch]
}
And now we can make as many stores as we want!
const [LayoutStore, useLayout, useLayoutDispatch] = makeStore(layoutReducer, {
menuOpen: false
})
const [TodoStore, useTodo, useTodoDispatch] = makeStore(todosReducer, [])
2.4 And now the cherry on top
But what about persistence? You ask.
What about it? I say and just add a few lines of code in our makeStore
function:
export default function makeStore(reducer, initialState, key) {
const StoreContext = createContext(null)
const DispatchContext = createContext(null)
let finalInitialState = null
try {
finalInitialState = JSON.parse(localStorage.getItem(key)) || initialState
} catch (e) {}
const finalReducer = (state, action) => {
const newState = reducer(state, action)
localStorage.saveItem(key, JSON.stringify(newState))
return newState
}
// And now we use finalInitialState and finalReducer
// instead of reducer and initialState
}
And this will give us persistence in all stores we make.
Hold on, isn’t this all client side? Yes it is. So in the next part, let’s see how we can connect our app to the server state and have it play well.