In a previous post, we demonstrated how the Container/Presenter pattern is a solid approach to managing your React state. This time we are going to look into using Reducer functions as the method to managing change in state of your components.
Reduce
Reduce (also known as a fold) is a functional programming concept that deals with the transformation of data structures using recursion and higher order functions. If you have used either the Array.prototype.reduce
or Array.prototype.map
functions, you already have experience with this technique.
The Approach
The general approach is to have a reduce
(can be named whatever you'd like) function that understands how to respond to certain messages and will output the transformed state. We'll normally call this reduce
function from a dispatch
function.
This is essentially the same pattern you use with Redux, but we don't need to install any packages to use it.
Let's examine the code snippet below.
function reduce(prevState, message) {
switch (message.type) {
case "ADD":
return {
items: [...prevState.items, prevState.newItem],
newItem: ""
};
case "REMOVE":
return {
items: prevState.items.filter(
(i, idx, prevItems) => idx !== prevItems.indexOf(message.item)
)
};
case "CHANGE":
return { newItem: message.item };
case "DISCOUNTS":
return { discounts: message.discounts };
case "INITIAL":
return {
newItem: "",
items: [],
discounts: []
};
default:
throw new Error("Unknown message type received");
}
}
class Cart extends React.Component {
state = reduce(undefined, { type: "INITIAL" });
componentDidUpdate(prevProps, prevState) {
if (prevState.items.length !== this.state.items.length) {
const itemsQuery = this.state.items.join(",");
ajax(`https://discountdb.com/?items=${itemsQuery}`)
.then(discounts => this.dispatch({
type: "DISCOUNTS", discounts
}));
}
}
dispatch = action =>
this.setState(prevState => reduce(prevState, action));
onNewItemChange = event =>
this.dispatch({ type: "CHANGE", item: event.target.value });
addItem = () => this.dispatch({ type: "ADD" });
removeItem = item => () => this.dispatch({ type: "REMOVE", item });
render() {
return (
<ul>
<h2>Cart</h2>
<input
type="text"
onChange={this.onNewItemChange}
value={this.state.newItem}
/>
<button onClick={this.addItem}>Add item</button>
{this.state.items.map((item, idx) => (
<li key={idx}>
<button onClick={this.removeItem(item)}>Remove</button>
{item},  
{this.state.discounts[idx]}% off!
</li>
))}
</ul>
);
}
}
Here our reduce
function resolves to a switch statement which delegates according to certain messages. We call this function in two places: directly in our state
initializer to bootstrap our component and in our dispatch
function to allow our event handlers to dynamically pass messages.
Unlike the function you pass to Array.prototype.reduce
, our reduce only returns the changes to state and not the entire new state. This is because we only pass changes to this.setState
.
Notice that the only place we call this.setState
directly is in the dispatch
function.
Benefits
What we have done is extract and enumerate the various transformations that can happen to our component state.
Isolating our state transformations this way can be beneficial when it comes to unit testing; our reducer is just plain JavaScript (no React). We fully extracted all state transformations into the reducer, but you are free to only pull out the ones that can benefit from the indirection.
I have added this technique to a React codebase that was written with no state management patterns in mind, and I found that it really simplified and focused each component.
By creating the dispatch
function, we are able to pass only one function as a prop to child components if they need to manipulate their parent's state. I found that this drastically reduces prop drilling.
Drawbacks
While this allows you to pass fewer callbacks to your child components, you will still need to thread it through your component tree if you want to modify top-level state from a leaf-node.
If you're implementing this pattern and think to yourself (like how I felt writing the above contrived example), "Why am I even doing this?" your component(s) might not be complex enough to warrant this.
Wrapping Up
This isn't an original concept; Dan Abramov has discussed this before and his article is also worth reading. This post is mostly an exercise in exploring different ways to organize and transform React component state.
Next time you are working with a React component, try to think of new ways you can work with state and let me know what you come up with on Twitter: @mitchhanberg.