Four patterns for global state with React hooks: Context or Redux

And the libraries I developed

Introduction

Global state or shared state is one of the biggest issues when you start developing a React app. Should we use Redux? Do hooks provide a Redux-like solution? I would like to show four patterns toward using Redux. This is my personal opinion and mainly for new apps.

Pattern 1: Prop passing

Some might think it wouldn’t scale, but the most basic pattern should still be prop passing. If the app is small enough, define local state in a parent component and simply pass it down to child components. I would tolerate two level passing, meaning one intermediate component.

const Parent = () => {

const [stateA, dispatchA] = useReducer(reducerA, initialStateA);

return (

<>

<Child1 stateA={stateA} dispatchA={dispatchA} />

<Child2 stateA={stateA} dispatchA={dispatchA} />

</>

);

}; const Child1 = ({ stateA, dispatchA }) => (

...

); const Child2 = ({ stateA, dispatchA }) => (

<>

<GrandChild stateA={stateA} dispatchA={dispatchA} />

</>

); const GrandChild = ({ stateA, dispatchA }) => (

...

);

Pattern 2: Context

If an app needs to share state among components that are more deep than two level, it’s time to introduce context. Context itself doesn’t provide global state functionality, but combining local state and passing by context does the job.

const ContextA = createContext(null); const Parent = () => {

const [stateA, dispatchA] = useReducer(reducerA, initialStateA);

const valueA = useMemo(() => [stateA, dispatchA], [stateA]);

return (

<ContextA.Provider value={valueA}>

<Child1 />

</ContextA.Provider>

);

}; const Child1 = () => (

<GrandChild1 />

); const GrandChild1 = () => (

<GrandGrandChild1 />

); const GrandGrandChild1 = () => {

const [stateA, dispatchA] = useContext(ContextA);

return (

...

);

};

Note that all components with useContext(ContextA) will re-render if stateA is changed, even if it’s only a tiny part of the state. Hence, it’s not recommended to use a context for multiple purpose.

Pattern 3: Multiple contexts

Using multiple contexts is fine and rather recommended to separate concerns. Contexts don’t have to be application-wide and they can be used for parts of component tree. Only if your contexts can be used anywhere in your app, defining them at the root is a good reason.

const ContextA = createContext(null);

const ContextB = createContext(null);

const ContextC = createContext(null); const App = () => {

const [stateA, dispatchA] = useReducer(reducerA, initialStateA);

const [stateB, dispatchB] = useReducer(reducerB, initialStateB);

const [stateC, dispatchC] = useReducer(reducerC, initialStateC);

const valueA = useMemo(() => [stateA, dispatchA], [stateA]);

const valueB = useMemo(() => [stateB, dispatchB], [stateB]);

const valueC = useMemo(() => [stateC, dispatchC], [stateC]);

return (

<ContextA.Provider value={valueA}>

<ContextB.Provider value={valueB}>

<ContextC.Provider value={valueC}>

...

</ContextC.Provider>

</ContextB.Provider>

</ContextA.Provider>

);

}; const Component1 = () => {

const [stateA, dispatchA] = useContext(ContextA);

return (

...

);

};

This is going to be a bit of mess, if we have more contexts. It’s time to introduce some libraries. There are several libraries to support multiple contexts and some of them provide hooks API.