Redux’ing without Redux
Dan Abramov’s tweet the other day got me thinking:
I wanted to reply to it, but found it very hard to explain my thinking in 140 characters 🙂. So I decided to explain my thought process here instead. I don’t really have an answer for how React state model can be improved; but have some thoughts on how some of Redux’s goodness can perhaps be utilised without actually using Redux.
We’ve found Redux very useful in the ReactJS application we’ve been implementing, in two specific cases:
- Where the state for a component sub-tree needs to be managed in complex ways, e.g. when there are multiple ways in which the state can be updated. Being able to see all the possible update scenarios to a piece of state, as pure functions in a single reducer module makes Redux very appealing in such cases.
- Where some state needs to be shared globally across multiple components in an application, sometimes even in different routes.
I’ll focus on the first use case in this writing.
Pure state transformation functions and a container component with local state
To make the explanation more concrete, let’s start by looking at an actual use case for which we used Redux in our application:
The form above is used for filtering property listings on https://m.realestate.com.au. The form state can be updated in multiple ways:
- Filters are updated based on user entries.
- Filters are reset when “Clear filters” button is clicked by a user.
- When user switches from one tab to another (buy, rent or sold), the value of location filter is copied across.
- On page load, the value of location filter is set based on user’s most recent search, if any.
- The locations filter can suggest user’s previous searches which upon selection would overwrite all the fields in the form.
The Redux reducer we implemented for this form looks like this:
I’ve excluded the implementation of actions for brevity.
There were a few things about Redux, which worked really well for us in this case:
- Removing state from all presentation components.
- Separating the business logic from React components.
- State updates implemented as easily-testable pure functions in a single module.
Thinking about it now though, there were also a few other properties of Redux which were not needed in this case:
- Being able to “connect” a component further down the component hierarchy to Redux store. We were happy in this case with a connected container component at the top level, wrapping the entire form and passing form state and action creators as props down to sub-components.
- A shared global store: We re-use the same search form component on other pages in our application, but don’t really need to share the state of the form with any other components on those pages. I.e. the form state could have simply been defined local to the wrapping container component to provide more encapsulation and to make it harder to leak this state to the rest of the application.
Getting back to Dan’s question, I feel like a simpler implementation without using Redux would have been one lacking these two attributes of Redux, while still providing us with the benefits listed above.
This somehow reminds me of the “Thinking in React” post:
Following the approach suggested in this post for implementing our search form would have given us a simpler implementation lacking the two attributes of Redux listed above which were not needed in our case.
In terms of the benefits we got from using Redux, this approach would still give us:
- Removing state from all presentation components. ✅
But would lack these attributes:
- Separating the business logic from React components. ❌
- State updates implemented as easily-testable pure functions in a single module. ❌
So I started thinking about how we can extend this solution to meet the above criteria without using Redux.
We can start by defining a module similar to the reducer snippet above providing pure functions for transforming the search form state:
This module is very similar to the Redux reducer module above, except that it provides a separate function for each action; which is actually nice since it removes the need for switch/case or if/else blocks.
So now we have a single module with pure functions for updating search form state and we need to somehow integrate it with our React components.
To do so, we can define a simple container component which wraps each of the functions provided by this module and creates new functions which can set the component’s local state based on the returned values from those functions:
This container component:
- uses local state for the search form data.
- sets the initial state of the search form in the constructor.
- wraps each of the pure state transformation functions, creating new functions which call the wrapped functions and set the component’s state based on the return value of those function calls.
- passes its state and the state updater functions as props down to presentation components.
So without using Redux, we can achieve the benefits we get from Redux ; but what’s more is that we would also avoid using a global store in favour of a component’s local state, which means that:
- The search form state can not leak to the rest of our application. It’s only available to a sub-tree of components where it’s really needed.
- We get to re-use the search form on other pages or in other applications without the need to pull Redux in or worry about where in Redux’s single state model the search form state is.
It’s worth noting that this solution would only work in specific use cases though, where
- The state of a component sub-tree does not need to be shared with the rest of the application.
- The component hierarchy for that sub-tree isn’t too nested, so the state and callbacks for updating the state can be explicitly passed down to sub-components through the component hierarchy.