kevin rutherford (@kevinrutherford) April 05, 2018
I’ve since been asked a couple of times to expand beyond tweet-sized soundbites and explain myself more fully. Here goes…
I’ve been using React, in both commercial and personal projects, since the middle of 2015; I’ve been using Redux since I heard about it in early 2016. I’ve only used them for client-side development. What follows is my personal, opinionated, idiosyncratic approach to React/Redux development, forged in half a dozen attempts to pursue test-driven development at a good pace. I’m a complete amateur when it comes to client-side development, and I expect I’ve made many choices that you might find laughable. I also know that what you are about to read violates much Redux “best practice”. Nonetheless, for the most part my code works, is well tested, and allows me to work at a reasonable speed. I’ll take that.
- I learned about React around 3 years ago and loved it right away. But then managing multiple stores became a real pain, so I’ve used Redux for absolutely every project in the last 2 years.
- I found the shared knowledge between action creators, reducers and selectors meant that changing the shape of state can be costly, especially if each of those things is unit tested. So I wrote a test framework that integrates action creators, reducers and selectors; this means I have high test coverage of everything outside components:
- (Note the use of deep-freeze to ensure that I am not accidentally mutating state.)
- I don’t test components. But I also don’t put logic in components.
- I have quite a lot of “containers”, and I don’t really distinguish these from “components”.
- I tried writing reducers that mirror the state of the server. But those rarely matched the needs of my views, and as a consequence I also had to write sophisticated selector functions mapping that state to whatever the views needed. The code in these selectors usually became complex — difficult to understand and change. So instead I now write reducers that prepare state specifically for consumption by particular components. I call such reducers “read models”, because that name helps me to remember what their responsibilities are.
- I don’t mind if two or more read models contain the same state. In fact, I expect it. Each does what it needs to do, decoupled from the needs of the other(s). Their code changes rapidly and independently. From the server’s point of view they probably contain de-normalised state; but the server’s point of view doesn’t matter here.
- Each read model is likely to also export a bunch of (tested) selector functions. This keeps my numerous containers simpler, and hides most of the detailed state structure from my tests.
- So, now that I’m thinking of my reducers as being read models, it makes sense to re-orient some other vocabulary: My action creators are now called “commands”, and I call the actions they create “events”.
- The names of my action creator functions used to consist of a random mix of past tense — eg. toggleSelected() — and imperative mood — eg. selectToggle(). But if I think of these functions as commands that may fail or be rejected, then only the imperative form makes sense. I like to use intentional naming too. So gotoSettingsPage() is better than toggleSelected() or selectToggle(), for example. This would create an event whose type is something like SETTINGS_PAGE_DISPLAYED.
- I find the “standardised” action format(s) to be huge overkill, so I avoid that whole ecosystem.
- I wrote a simple API middleware that “just works”. It also provides hooks (commands, in fact) allowing my tests to inject events that represent fake replies from the server. This means that my simple test framework (above) can be used to test the whole dispatch-middleware-store-selector stack. And this in turn means that I can often reorganise state without breaking tests or components.
- I use thunks. I prefer them to sagas, which I find difficult to test.
- I organise the app into bounded contexts, usually mapping to the “sections” or “pages” in the user’s mental model. Each of these is a source folder containing components, commands, read models, and tests. If necessary some larger contexts may have sub-contexts, indicated by sub-folders. I definitely don’t use folders for “classification” — eg. there is no reducers/ folder anywhere.
- I don’t use constants for event types, because I don’t want my read models to be coupled to knowing which commands — or even which bounded context — created an event. In the early stages of development I frequently move things around as I’m discovering where the context boundaries lie, so minimising the number of imports saves me a lot of time. My tests will usually catch any typos.
Again, I’m not claiming this to be a “good” approach, but it works well for me. It is evolving all the time, and will likely be quite different again in 6 months’ time.
Most of the code I’ve written using these mad ideas is commercial, and thus not shareable. But if you want to see what some of my ideas look like in practice, take a look at https://github.com/xpsurgery/penny-game (you can also visit http://xpsurgery.com/resources/pennygame/ to see the simulation running). This is the project that turned me off sagas, but it is also where I experimented with (nested) bounded contexts most thoroughly. Note also that I wrote this code before I had the epiphany regarding read models etc.
What do you think?