Finding `state`’s place with React and Redux.

The React/Redux landscape is still relatively young. Books are only now being published, and best practices are hard to come by. The documentation for both React and especially Redux are outstanding, but in trying to actually build something I came across more than a few practical questions which I had to reason through myself.

This post discusses what I found to be a reasonable way to use `state` with a React/Redux application. I post it here with the hope that those who have more experience with these tools can either comment why they think I’m wrong, with potentially better ways; or why they think I’m right.

Project

All of the code and screenshots shown are from my own “booklist” side project. The idea is a web application where you can scan in books — with information fetched from Amazon — view them, categorize them, etc. This is a fun project for me with the sole purpose of learning various new technologies which I poke at with what little free time I can muster. Expect styling and completeness to be rough around the edges, to say the least.

Use Cases

Books are listed in a device-optimized UI. The mobile UI renders books in an unordered list, with minimal information visible at first, with details expanded on click. The desktop UI renders a full-fledged table, where books can have their associated subjects modified one at a time, or in bulk via a multi selection. Both are implemented with the help of state.

Whether a more skilled responsive-design practitioner could make a single UI work for both is a question I’ll leave for another day.

Using state to expand a simple UI

The (still-roughly-styled) mobile/tablet UI looks like this

Clicking an item expands the details and shows the cover. Keeping an `expanded` property on each book, with a redux action getting dispatched up, just so an updated book could be sent back down, with its `expanded` property toggled seemed overly complex and obtuse. `state` seemed like a much more natural fit, and the code seems to work well.

for small UI things like this, a simple

toggle(){
this.setState({ expanded: !this.state.expanded });
}

seems more natural than a Redux dispatch.

Opening a desktop modal

Books in the desktop UI can be checked / selected, which dispatches a redux action and updates the relevant book object’s `selected` property. With one or more books selected, a modal can be opened to set those books’ subjects. Each book also has an edit subjects button to edit that book’s subjects.

The rough-on-the-eyes UI follows

I believe managing this through Redux would require the following:

  • Each subject in the store would now support `selectedForModalAdd` and `selectedForModalRemove` properties.
  • The flux store would add in a `editTheseBooksSubjects` array , as well as a property controlling whether the modal is showing.
  • A new dispatch action to edit subjects for a single book. The action would presumably pass the actual book, and the reducer would return a new state with the `editTheseBooksSubjects` containing just the one book, and the modal property set to true causing it to show.
  • A new dispatch action to edit subjects for whatever books are selected. The action would filter the store’s selected books and set `editSubjectsForBooks`, and set the modal property to true.
  • A new dispatch action to close the modal.
  • Separate dispatch actions to toggle `selectedForModalAdd` and `selectedForModalRemove` as the user checks and unchecks the subjects from either list. Although if desired this could probably be reduced to just one action if you don’t mind pass a string for which property to set in the action.

The relevant state-based code is

It’s by no means a simple one-liner — and on final proof-read I see that `singleBookSubjectsModal` and `multiBookSubjectsModal` are ripe for refactoring — but it does seem more natural to me than the flux alternative. These actions all deal with the internal state of the list component, and how and when its modal shows. It seems reasonable to me to therefore keep these behaviors in the component’s `state`, managed by methods on the components prototype. The result seems relatively simple and straightforward.

Potential objection

It’s not testable!

And I agree; it’s not. But are these really the unit tests you need to be writing? That a flux dispatch to set `selectedForModalRemove` on subject with _id: 123 … sets the property `selectedForModalRemove` on subject with _id: 123? The TDD advocates will likely say yes, and that’s fine; but I find these tests to be more work than they’re worth. This is code that exists in one place, which does a simple task. Moreover, assume for the sake of argument that all of this was in fact handled in Flux, and tested. Assume further that the Flux code is refactored, and the tests still pass. Are there any developers among us who wouldn’t re-run the module to make sure everything still … works? If you would, as I would, then what have those tests really bought you? I’m a much bigger fan of focusing my testing efforts in areas with lots of intricate edge cases and branching logic.

For example, these subjects will eventually be arbitrarily nestable. So a subject hierarchy might look like History > American History > Antebellum > Slavery. I’ll likely store materialized paths in Mongo; the load subjects action will likely return a flat list of all subjects, with me being responsible for manually stacking them based on regex checks on the path values, before sending them back down the Redux store. This seems the ideal type of situation to unit test; the high number of edge cases can best be fleshed out through a well-constructed test suite.

Potential objection

You’ve violated the single source of truth

The subjects listed in that modal are no longer singularly determined by the props sent down from Redux, but instead partially cached away in state.

For example, if you delete one of those subjects, and the subject is removed in the callback to your ajax request, and your network is exceedingly slow, and you manage to open the modal, and check the soon-to-be deleted subject, the subject will continue to show in the running list of checked subjects, but will now be removed from the checkbox list, so you can never uncheck it. Something like this

Assuming for the sake of argument this is a likely enough scenario to care about, there are (at least) two possible solutions:

1: Don’t remove the to-be-deleted subject asynchronously

Just remove it when they click the delete button, and have an error callback to let the user know that there may have been an error deleting the subject. Why not optimize for the 99.99% case?

2: Just use Redux

Just make a separate reducer for this family of actions, and combine it with your main reducer, which Redux makes simple. The additional code needed vis-a-vis the state alternative wouldn’t be that much, and minimizing key strokes should never be a goal when writing code.

Using `state` for things like this has pro’s and con’s like anything else. The state-based code does seem more natural and simple, but splitting off from the Redux store does have potential consequences which need to be understood and considered.

Thoughts?

I posted this with the sincere hope that some people who’ve been using these tools for longer than I have (which wouldn’t be saying much) might be able to provide some feedback.

If for some reason you want to see the full code, it’s here. Again, many bits are laughably incomplete. Also, running this project would require some work: you’d need to add in a required but not-checked-in module with AWS credentials, and of course create a MongoDB instance with the appropriate collections.

UPDATE

Since writing this, I’ve learned a bit more about Redux. Namely, I’ve learned that actionCreators are added directly to your component’s props, and that Redux stores are best kept with flat, normalized data, with selectors sitting in front of them to shape the data as needed. I wrote about these things separately here.

I rewrote the second of the use cases above (modifying multiple books’ subjects) to use Redux, instead of the components state, and I’m quite happy with the result. Essentially, the books subject modifier has its own reducer, which stores whether a single book is having its subjects changed; a mutually exclusive boolean flag indicating whether all currently selected books are having their subjects changed; and then separate objects which store hashes of the subjects currently being added, or removed.

The reducer, with initial state looks like this

Then, to turn this flat data into something useful, I’m using these memoized selectors created with reselect.

modifyingBooksSelector analyzes whether a single book, or the currently selected books are having their subjects modified, and then projects those relevant _ids into an array of the actual book objects. (state.books, the third parameter to the selector function is a flat object hash relating book._id -> book object.

addingSubjectsSelector and removingSubjectsSelector both similarly relate the currently selected subject _ids to actual subject objects. And then booksSubjectsModifierSelector puts all of these pieces together into a single object representation of the state. And of course other code composes this selector into the rest of the store (the books store, the subjects store, etc).

By my count, the new code is about 4 lines longer than the old, though at least 10 of those lines are just reselect boilerplate. Also, not shown, there’s a few other lines to define the basic Redux boilerplate for the new action names and action creators.

Conclusion?

I think the new code is significantly simpler and easier to understand, to say nothing of the fact that it’s now completely testable, and integrated with the rest of the Redux store, and therefore not vulnerable to the bugs I described earlier (race conditions with other actions that modify the store while you open the modal to modify these subjects). I think the few extra keystrokes needed are well, well worth it. I’m quite happy with how simple things become when storing flat, normalized data in the Redux store, with selectors querying the store as needed.