The React Hooks based alternative to Redux and the Flux pattern

Stefan Nagey
Capbase Engineering
6 min readJan 12, 2019
There is a better way than Redux. Hooks + Context FTW

In this article, we will be looking at how to implement Redux, Saga, Thunk, and Flux based thinking with the pure functional programming power that comes with React Hooks.

ICYMI, be sure to also check out Part 1: React Hooks is the functional paradise you’ve been waiting for and Part 2: Local State management using React Hooks. Also be sure to check out the forthcoming Part 4: Asynchronous Functional Programming with React’s useEffect hook, and Part 5: Gotchas of working with React Hooks and React’s new Concurrent Mode.

Handling large, complex, states easily with the useContext and useReducer hooks

In the the previous parts, we’ve seen where we can easily handle states in a single component. Let’s look now at how we can approach this over multiple components.

Let’s say that we’ve got a list of products, and we want to render detailed data about them. Let’s imagine a markup that looks something like this:


const ProductList = ({ products }) => products.map(
product => <Product data={product} />
);
const Product = ({ name, description, price }) => (
<dl>
<dt>Product Name:</dt>
<dd>{name}</dd>
<dt>Product Description:</dt>
<dd>{description}</dd>
<dt>Product Price:</dt>
<dd>{price}</dd>
);

Now, this is pretty straight forward, we’re getting props from upstream, and we’re drilling them down.

Works fine, but it might be that we want to do more complex things, or we want to update the product state and have it propagate upward.

Let’s first put our state into a Context, called ProductsContext. First we create the context, easy, like this:

const ProductsContext = React.createContext();

A context is just a bucket that can store an object (again, think like a struct) in a property called value.

We could just shove our products variable in here, and that would work pretty well. Let’s see:

const ProductContext = React.createContext();const ProductList = ({ products }) => {return products.map(product => (
<ProductContext.Provider value={ {product} }>
<ProductDisplay />
</ProductContext.Provider>
));
const ProductDisplay = () => (
const { product } = React.useContext(ProductsContext);
return <Product data={product} />
);

This is pretty neat. It makes it so that we don’t have to drill the product list down, and also makes it so that we could easily have multiples of these at any level.

I could’ve also just used this context directly in the Product component (but I didn’t, just so that we don’t have to edit that one).

Managing Context values with useReducer

So, now the obvious question is, “how do we change the values in the context?” — for that, the React team has given us a great little tool, useReducer.

useReducer is just like useState in that you get back a value that stores your state, and a function pointer that can be used to update that state (just like dispatch in redux world. Where it differs is that not only does it take an initial value, but it also takes a second value for a middleware, or reducer (to borrow from the redux terminology).

This middleware function will receive both the current state, as well as the argument passed to the dispatcher, and is expected to return a new value for the state.

With this, we can now store both this state and this dispatch function in the value of the reducer, so that in any downstream component, we can modify the state by calling into the dispatch method.

Let’s look at an exceedingly simple method of modifying our ProductList, adding a product, and removing a product.

A simple middleware

Let’s have a look at a simple middleware that is just adding or removing a product from our context.

We’ll use the familiar redux convention of an action that as an object that has two properties, a type and a payload. The type is a way to communicate to the middleware reducer which action you want to take, and the payload is the data necessary to carry out that action.

In this example, I’ll split the method to reduce each type of action into its own function, but there’s no reason you can’t keep this all in the body of the reducer. I’ll show you one of each

Let’s look at the code:

const middleware = (currentState, dispatchArgument) => {
switch(dispatchArgument.type) {
case ‘changeName’:
return changeProductName(currentState, dispatchArgument.payload);
case ‘changePrice’:
return { …state, price: dispatchArgument.payload }
};
};
const initialState = products;const [state, dispatch] = React.useReducer(middleware, intialState);

Now, if we want to change the price of a product, it can be as simple as this:

dispatch({ type: ‘changePrice’, payload: 3.50 });

This is great, but how do we get that to propagate down through the tree? Let’s look at how we hook this up to our context.

Hooking useReducer together with useContext

Here we’re going to change our previous call to ProductContext.Provider to take use the return values of our useReducer call from the previous section as its value. This will give us, in our context, both a value, and a way to change that value.

To make that easier, let’s make a method that wraps the previous code so that we can easily access it.

const useProductReducer = (product) => {
const middleware = (currentState, dispatchArgument) => {
switch(dispatchArgument.type) {
case ‘changeName’:
return changeProductName(
currentState, dispatchArgument.payload
);
case ‘changePrice’:
return { …state, price: dispatchArgument.payload };
};
};
const initialState = product; const [state, dispatch] = React.useReducer(
middleware, intialState
);

return { state, dispatch };
};

OK, now we’ve create a simple function useProductReducer that will take a list of products, and will return a value we can use as the value for our context.

Write your own hooks!

The eagle eyed among you might notice that we call this use similar to the react hooks. That’s no accident. Congrats, you’ve just made your first hook!

A React hook is just any function that starts its name with use. Good hook hygiene requires you only call hooks from the function body (not within loops or conditionally), and that applies to hooks you create as well. Hooks you create are often most useful when they call other hooks, as we have here.

Now, let’s integrate our hook with the code for the context above:

const ProductContext = React.createContext();const ProductList = ({ products }) => {return products.map(product => (
<ProductContext.Provider value={useProductReducer(product)}>
<ProductDisplay />
</ProductContext.Provider>
));
const ProductDisplay = () => {
const {
state: product,
dispatch
} = React.useContext(ProductsContext);
return <Product data={product} />
};

You’ll notice here that we create a new context with its own reducer for each product, and then within that component, we can access the context and change the product.

Now, the way that we’re changing the product isn’t spectacularly interesting, but we can extend this example to modify a shopping cart, or to send items to a backend or some other remote.

What comes next

In this piece you’ve learned how you can use the useReducer and the useContext hooks to effortlessly manage huge state trees, and to move past the patterns of flux and redux into the functional, stateless world of react hooks.

In the next part of this series, we’ll look at working with remote and back-end systems using react hooks, and how to manage asynchronous workloads with react hooks. Following that, the final part in this series will deal with the various gotchas of working with React hooks.

If you missed it, please check out the first part of this series, in which we discuss the importance of functional programming, and how stateless components can lead you to the promised land, and part 2, where we discuss how we can manage state in stateless components using the useState hook.

About the Author

Stefan Nagey is Co-Founder of Capbase and Dharma. In a previous life he has variously promoted motion pictures, been a diplomat and registered foreign agent, and run payment processing in the online gambling space. He variously writes about engineering and the headaches of being a startup founder in the 21st century.

--

--

Stefan Nagey
Capbase Engineering

Data geek. Capbase.com and Dharma.ai Co-Founder. Passionate about scalability, governance, cycling, and poker.