Solving Redux’s shortcoming in 150 LOCs
This week I flew down to Peru for a job interview. Most interviews consist of trite questions like, “How do you do a depth first search?” I’ve made it a habit to walk out of those interviews early because if that’s the hiring criteria, my coworkers are guaranteed to be world-class problem solvers… at solving solved problems.
Thankfully, this one was fun. We decided to scour the redux ecosystem to find all the pain points and spend a couple hours to pair-program a solution and solve them all in one go. If you want to follow along, we posted our proof of concept using counters here: https://github.com/mattkrick/redux-operations-counter-example.
Problem #1: Two States, One Reducer
This is an common one. If I have 2 counters, I shouldn’t need 2 reducers to manage them. Elm solves it fairly easily in their example #2, but nothing exists for redux. There’s a solution for it called multireducer, but implementing it isn’t terribly easy and requires leaving vanilla react-redux. We like vanilla.
Problem #2: Listening to other reducers
Now things get fun. Let’s have a state that listens to other actions. Building on Problem #1, we want to listen to certain actions and do something when we get those. For example, increment a number when any counter’s INCREMENT is dispatched. Tomas Weiss summed up the problem is his post Encapsulation in Redux.
Problem #3: Prioritized responses to actions
Sometimes action execution is important. Currently to prioritize it, you have to carefully order your reducers, but this gets brittle. For example, ReducerA.INCREMENT needs to come before ReducerB.INCREMENT, you load ReducerA first. But what if ReducerA.DECREMENT needs to come after ReducerB.DECREMENT? You gotta break your reducers into single actions. Gross. To complicate this problem, you might even need that interim result to compute the final result…
Problem #4: Use the result of a previous action in a future computation
As shown in Problem #3, when we trigger a series of actions, each result should be cached and given to the next action. This is currently solved with generators and sticking logic in the middleware. For example, redux issue #1315:
function* c(state = 0, action) {
const aState = yield a
const bState = yield b
return state + Math.abs(aState - bState)
}
Neither of us are fans of sticking business logic in middleware, and generators aren’t always easy to reason through, so we overloaded the action itself to keep it at the application layer. This keeps the logic easy.
Problem #5: Dynamic state instances
Sometimes, state shape is defined by the user at runtime. A great example is how redux-form allows for adding fields dynamically to a form. As you can see above, we had some fun with this one and went meta.
Problem #6: Large Teams
Every time dispatch is called, every reducer in your combineReducers is called. This is usually fine, but what if that jerk on your team wrote something stupid before the switch statement in a bad reducer? He just borked your entire app. Instead, we introduce the concept of operations (a head nod to the lexicon used in GraphQL). One action has a series of operations, which are executed in a defined sequence. That’s what makes solving Problem #4 so darn easy. Of course this could get hard to debug. If only there were a visual API like GraphiQL….
Problem #6: There’s no visual API like GraphiQL
We’re both huge fans of GraphQL, especially that little side window in GraphiQL that lets you explore your schema API. It shows you arguments, descriptions, and a bunch of useful stuff. We decided to add that same functionality to redux devtools. With this tool, you don’t need to understand your entire app to understand a single action. We like that. Say I want to call SET_COUNTER. What does it trigger? In what order? Where does it go in my state? Does it take args? What do those args look like? Let’s find out!
Problem #7: Fully backwards compatible
Creating your own Flux flavor is so 2015. Redux won. So, we wanted to make sure that whatever we built could function in parallel with existing code, 3rd party redux packages like the great react-redux-router and redux-forms, and of course the amazing redux-devtools. We also ended up solving the thunk/promise problem in the process by keeping logic in the action creators.
Closing Remarks
Our final package is called redux-operations and is available on npm and github. It weighs in at 150 lines of code. It’s still very much in alpha and we would love your feedback to make it even easier. We’ve already got a few ideas, but opted for a verbose API over a magical one for this alpha release so you can easily grok how it works.
All in all, I’m really excited about what we built. I also love that we built it during an interview. Imagine if every interview solved a github issue or contributed something of value to the OSS community. If you’re a hiring manager, you have the power to improve OSS, learn exactly what it’s like to work with the candidate, and make the best hiring decision possible. That’s even more exciting than what we built.