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.

    • More recently, much of this concern was largely mitigated by @reduxjs/toolkit (GitHub).

    • However, it still requires a fair amount of domain knowledge about how Redux works.

  • 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

  1. Clone GitHub - adamstankiewicz/hooks-context-selectors: Code examples to demonstrate React patterns around hooks, context, and context selectors.

  2. npm i in the root to install node_modules

  3. npm start to run the application at http://localhost:8080

Examples

Redux Example

Source

  • Because the example repo is using @edx/frontend-platform to bootstrap the MFE application, we may pass a Redux store to theAppProvider component in src/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 with useSelector from react-redux.

    • Each counter component subscribes to the Redux store by extracting the respective count value using useSelector and handles decrement/increment clicks by dispatching (via useDispatch) 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

Source

  • 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)

Source

  • 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)

Source

  • 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 utilizes use-context-selector to create and read from the counter-related context for the route.

  • 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.