Redux’s dispatch / action / reducer architecture makes it easy to reason about complex state management. Plus the dev tools and overall community support is fantastic.
However, Redux is overkill for state that’s local to a specific component (e.g. is this dropdown open?). Redux’s state management is, by design, independent of React’s component architecture. Separating presentation logic from your state management code is great, but a standard React-Redux implementation (even using the helpful react-redux library) still requires a lot of glue code to hold everything together. Even simple state changes require all of the the following:
- An action to represent changes to the component state
- A reducer to apply the action to our store state
- Code to hook the reducer to the store
- The React component itself
- Container code hooking up the React component to code that dispatches our actions
The simplest alternative to this is to just use React’s setState
function. But this deprives us of Redux’s dev tools and other benefits.
To address this, I spent part of this weekend writing react-redux-set-local. The name’s a mouthful, so let’s call it RRSL.
RRSL provides a way to connect an isolated portion of a Redux store’s state to your component while still maintaining separation between presentation and state management. Here’s what it looks like:
import { connect } from "react-redux-set-local";
// Presentation (component)
const DogShow = (props) => <div>
<div>
<span id="dogs">
{props.dogs} {props.color} dog{props.dogs === 1 ? "" : "s"}
</span>
<button id="woof" onClick={props.onWoof}>
Woof
</button>
</div>
</div>;
// State management code
const mapToProps = (localState, setLocal, ownProps) => {
localState = localState || { dogs: 0 }; // localState can be undefined
return {
...ownProps,
...locals,
onWoof: () => setLocal({ dogs: locals.dogs + 1 }, "INCR_DOGS")
// INCR_DOGS is provided as a descriptive type string. It's used
// as the type in our action log for ease of debugging but has
// no inherent purpose
};
};
export const Container = connect(mapToProps)(DogShow);
Under the hood, RRSL is really combining two ideas.
Isolated State
The first is isolated state. React components by default are easy to compose and nest. React children don’t need to know anything about what their parents are doing. And parents just need to know what props to pass down to their children.
Redux breaks that because child containers need additional guarantees from their parents that the actions they’re dispatching can be handled by the store in the current context.
Previous approaches to this, such as redux-react-local and redux-fractal, address this with container HOCs that (1) isolate the portion of our Redux state that’s actually relevant to our current component and (2) take reducer functions and actions that apply only to that isolated portion. The result is these make it easier to co-locate logic about state management and how that state is presented in the same file (or folder if things start getting really complicated).
Like those packages, RRSL also isolates our Redux state. But rather than provide a localized dispatch function or reducer, RRSL’s connect
HOC provides a setLocal
function that replaces the isolated state with a new state.
Reducing Reducers
This second idea – that we should just specify state changes explicitly rather than write reducers – isn’t new. It’s basically what React’s original setState
does. All we’re doing is shoe-horning setState
into Redux by dispatching actions that specify exactly what our new local state should look like and using a single common reducer to process all those actions.
There’s been plenty of back and forth over whether (or when) de-coupling action creation from reducers is a good idea. While there are plenty of reasons for doing so, the big one is that in very complex apps, we want multiple reducers to respond to a given action. That is, a single action updates different parts of the store. Or maybe different stores if we’re dealing with distributed state.
That rationale doesn’t apply to RRSL. Remember, we started with the premise that we’re dealing with isolated, local state. By definition, we’re only targeting a single part of the store, and it happens to be the same part of the store that’s used to populate the props on our presentational component. Given those constraints, writing a reducer that targets only one part of our store (and that’s probably co-located with a single component class) is unnecessary.
Keyed State
Notwithstanding its basis in isolated state, RRSL comes with the option to persist and synchronize state across multiple component instances:
export const Container = connect(mapToProps, {
key: (props) => props.color,
persist: true
})(DogShow);
let c1 = <Container color="blue" /> // Identical to c2
let c2 = <Container color="blue" /> // Identical to c1
let c3 = <Container color="red" /> // Independent of c1 and c2
The idea here is to use react-redux-local-state
as a basis for managing state that might span multiple component trees (which isn’t very doable with React’s native setState
).
One thing I may look into the future here is making it easier to create containers that create multiple keys from a set of props and can access and manipulate multiple parts of a store.
But that starts taking us back to the aforementioned discussion about why we separate reducers and actions in Redux in the first place. While handy, sharing state between multiple component instances (and possible very different component classess at that) makes it harder to reason about which instance or class is modifying our state and why. But it’s something to think about.
RRSL is still more proof-of-concept that production-ready library right now, but it’s not too complicated and takes advantage of the performance optimizations in the already rock-solid react-redux
library. There aren’t any obvious show-stoppers, so feel free to use it!
Related Reading:
Comments