Opinionated opinions on code organization of a React-Redux project

← back to the blog

I've been working on a large-scale React-Redux project for several months now. React is not a framework and so it doesn't enforce a particular way of grouping related code or related files. I've been thinking about what it means to provide forward-maintainability in this type of codebase and how we can make our code understandable to other human beings (ok, all human beings, including the original programmer). I thought I would share some of what I've learned (often the hard way) and see what other people who've faced this think.

It seems pretty normal to have one top-level folder for actions and another for reducers, but I've decided that this isn't great. For one thing, both actions and reducers need access to the same set of constants. For another, large stores have a need for a type of "store-query" function that doesn't really belong in either place. I've ultimately decided that a better organization is to have a single "store" folder containing a set of files which export separately their constants, actions, reducer, and queries. Each part of the store is then grouped together in one file making it easier to understand what its purpose it and cutting down on import boilerplate.

2. Test your store

Action creators (at least synchronous ones) and reducers are extremely easy to test and the benefits of testing therefore greatly outweigh the costs. Redux's combineReducers method makes it easy to write reducers that only need to know about a single section of the store, limiting the amount of state we have to mock in our tests. For example, to test an ADD_TODO action we could write:

import todosReducers, {constants as c} from '../todos'

it('should add a TODO', () => {
    const initialState = []
    const finalState = todoReducer(initialState, {type: c.ADD_TODO, todo: 'Write unit tests'})
    expect(finalState).toHaveLength(1)
})

While our application state will surely be more complex than a single array, the todosReducer doesn't care. These types of tests are trivially easy to write, and the process of writing them will solidify the design of the reducers as well and guard against regressions.

3. Redux middleware is powerful

Redux is not about fetching data, it is about predictable data management. Yet the two are so closely intertwined that you basically cannot have one without the other. The "de facto" solution for binding asynchonous calls to redux is redux-thunk, which allows you to dispatch a function which is passed dispatch as an argument.

Did you know that the source code for redux-thunk is only 14 lines? It does its' job because the middleware interface for Redux is so well designed: if the middleware sees a thing which matches some criteria (in this case: it's a function), it is able to dispatch other actions (in this case when the function says so).

But there isn't any reason to stop there: middleware that sees a promise can dispatch when the promise resolves (or errors); middleware that requests geolocation could dispatch when the user gives permission. There are a myriad of middleware libraries to choose from or you can write your own, and the advantage over thunk is that they make it easy to test your action creators. If your app uses a particular type of data and you know how you will be requesting it, you can use ONE hard-to-test middleware that interfaces with your now easy-to-test action creators, of which you will have many. Going with thunk alone is simple to begin, but testing it sucks (the recommended approach is to mock the entire store/state).

For example, suppose all your app's data is fetched over REST. You could write (or pick) a middleware that monitors for objects that look like this:

{
    type: 'REQUEST',
    method: 'GET',
    url: 'http://example.com/api/send'
    query: // ...
}

Your action creators now spit out simple objects which makes them a piece of cake to test. Your middleware looks for these "actions" and then dispatches when they are made, when they resolve, and when they error. The middleware is harder to test but once it works, it works (and chances are a library already exists).

4. Immutability helpers: the best kept secret in state management

I've extolled the virtues of the immutability helpers in a (http://caseyy.org/blog/using-immutability-helpers-for-redux-reducers)[previous post] - I really like this solution for immutable state management and haven't felt the need to go all-in and use Immutable.js. Yet.

5. Nest as deeply as you need to

When I took over this current project, all of the components were kept in one folder one level deep. This does not ... scale. When you have hundreds or even dozens of components, it becomes hard to find what you're looking for. I've found a better approach is to make any "parent" component the index file in its own folder, with all of its subcomponents as separate files. If/when those subcomponents become components, they are moved to directories (of the same name) and become index files. This can create some very deeply nested folders, but it's much better than the alternative. I also have a top-level "partials" folder with shared/reusable components.

As far as the annoying require('../../../../...')-like statements you tend to get, there are some possible solutions to that, too.

Shortly before I published this, this article came out from Hacker Noon, which dives into this whole topic a lot more deeply.

6. Test components judiciously

Generally speaking, it's easiest to test code that is free of side-effects, and React components fail this litmus test (by their nature, they "mutate" the DOM). There's also a litany of ways that a UI can look "broken" while producing perfectly valid markup and error-free processes, so it's arguable whether any amount of automated testing can eliminate regressions entirely. Still, there can be advantages to testing components, especially when a component is used in a wide variety of contexts throughout the application. Reusage of components provides more opportunity for something to go wrong in one context, and a greater need to clearly define the contract of the component, increasing the value of both unit testing and TDD.

7. PropTypes help to self-document a component's responsibilities

Speaking of defining the responsibilities of a component, PropTypes are just a godsend here. Not because they provide any safeguard against bugs, or are a replacement for static typing (they aren't and shouldn't be), but for the sheer value of writing out the PropTypes any component will accept. They are more documentation that anything else, with the added benefit that React will warn you when your expectations fail.

An additional nugget that took me way too long to learn about was using PropTypes.shape to define objects with particular properties. It can a little tempting to go nuts here, so I settled on defining objects down as far as any particular prop that will be accessed within that component itself, and then further deconstructing it inside of any child components as necessary.

8. Any component which can be functional, should be functional

Jacques Favreau made a compelling argument recently that React jumped the gun in deprecating createClass in favour of ES6 classes, and I'm tempted to agree. But that is a secondary consideration to the fact that functional stateless components are just easier to understand and reason about. Unless you have a compelling reason for storing state in a React component instead of in Redux, do so. Unless you have a compelling reason to use a lifecycle method, do not use a lifecycle method. For me, the overuse of shouldComponentUpdate, componentDidMount, and componentWillReceiveProps is a code-smell: it maybe means there is something more deeply wrong about this part of the component tree and it should be rethought.

9. JSX is more like HTML than it is like JavaScript

Another use I have seen for class methods in React is calling out additional "render" methods from the main render function. So, instead of placing all JSX in one render, the render calls renderSomePartOfTheDOM. I believe the idea behind this pattern was that it keeps code compartmentalized and the various render functions "doing one thing", but I personally find it impenetrable spaghetti. JSX should be "templating", it should look like HTML and HTML is easier to understand when it's obvious where a given set of tags fits into its environment.

If you think about templating languages like Handlebars, they have "reduced" features compared to Turing-complete languages because you only need a few things: loops are handy, so are conditionals, and the ability to "include" other templates is essential. JSX allows you to "jump" back into JavaScript at any point using curly braces and call other functions if you want to, but any expression must be complete before the closing brace. So map works for looping, while ternary expressions are my go-to for conditional statements. Any more complex logic should probably be handled outside of the JSX entirely.

Conclusion

I have endevoured in this post to be opinionated but not preachy: I'm genuinely curious to know how others handle these problems and if anyone disagrees with me. The flexability of React is both a blessing a curse, and I greatly enjoy working in a space where there are so many ways to skin a cat.