Patterns with React Hooks & Context (compared to Redux)
Overview
Redux is a popular way to manage state for React applications.
Typically a global store (read-only).
Store is mutated by dispatching actions.
Supports extracting data from the store from any nested component in the component tree (i.e., no prop drilling).
Redux historically has had a lot of boilerplate and was a bit difficult to reason about.
Much of what you can do in Redux can also be done with (nearly) vanilla React hooks & context.
The below examples show 4 approaches to building the same UI behavior:
Basic Redux
Basic hooks + context
Hooks + context (with
useReducer
)Hooks + context (with context selectors)
Caveat: performance and re-rendering
When a component subscribes to any property of a context value, that component will re-render even if that component doesn’t use any of the properties that may have actually changed. That is, an update to one component may trigger re-rendering of other components unintentionally, depending on how your components are structured.
The final example with context selectors solves for this “context re-rendering problem”.
Examples Code Repository
Each example below renders UI behavior that are functionally equivalent from a user’s perspective, but differ in terms of implementation pattern and re-rendering implications.
A random number is computed for each component cycle to more easily visualize when certain components get updated.
Instructions
npm i
in the root to installnode_modules
npm start
to run the application at http://localhost:8080
Examples
Redux Example
Because the example repo is using
@edx/frontend-platform
to bootstrap the MFE application, we may pass a Redux store to theAppProvider
component insrc/index.jsx
The Redux
store
is defined./src/examples/basic-redux/store.js
Adds a property/namespace
counter
to the store, controlled by a counter-specific reducer.The reducer is defined in
./src/examples/basic-redux/data/counterSlice.js
Created with
createSlice
from@reduxjs/toolkit
, which generates actions to mutate the data in the slice.
The reducer file also exports selector functions (e.g.,
selectCount1
) to use withuseSelector
fromreact-redux
.Each counter component subscribes to the Redux store by extracting the respective count value using
useSelector
and handles decrement/increment clicks by dispatching (viauseDispatch
) an action.When an action is dispatched, the reducer handles the action type and mutates the data existing in the store accordingly.
Only the random number for the counter that was interacted with is updated when the counter is decremented/incremented (no-rendering of the other, unrelated counter).
Basic Hooks + Context Example
Wraps the rendered route a React context provider where the current counter values are stored and accessed from any nested component under the context provider.
The current values of each counter are stored in independent state variables, and passed through the (memoized) context value along with decrement/increment functions for each counter.
The decrement/increment functions simply call the state setters for each count variable.
Both counters are subscribed to the same context, but read different values from the context.
Only the components nested under a context provider that explicitly subscribe to the context via useContext
will be re-rendered when the context value changes.
The random numbers for both counter are updated even though only one counter was interacted with (unnecessarily re-renders the unrelated counter).
Hooks + Context (with useReducer
)
Wraps the rendered route a React context provider where the current counter values are stored and accessed from any nested component under the context provider.
Rather than defining a count value for each counter in independent state variables, this example defines a reducer with an initial state containing both counters' initial value of
0
. It also defines actions that may be dispatched to mutate each counter value.The current values for each counter as well as the reducer’s
dispatch
function are exposed in the context value for descendent components.Both counters are subscribed to the same context, but read different values from the context.
Hooks + Context (with context selectors)
Wraps the rendered route a React context provider where the current counter values are stored and accessed from any nested component under the context provider.
As opposed to using
useReducer
in the provider, this example instead utilizesuse-context-selector
to create and read from the counter-related context for the route.Github: GitHub - dai-shi/use-context-selector: React useContextSelector hook in userland
For details on how
use-context-selector
works behind-the-scenes, see
Expects the resulting value from React’s
useState
hook to be passed into the context provider’svalue
.
Defines a custom React hook
useCounterActions
to return the relevant decrement/increment actions for each counter as helper for components to mutate the context value.Subscribes to only the relevant slice of the context value in each counter.