Exactly how to structure your React / Redux applications is a contentious point, a quick Google on the subject will bring up a myriad of recommendations and methods so I thought, why not add to the pile and throw my own recommendation into the ring?
React and Redux themselves are both pretty unopinionated on application architecture, Dan Abramov himself had this to say on the subject. Whilst I entirely agree that what works for you is generally the best approach to problems like this, I also believe that employing a consistent approach within organisations is important – it allows our developers to move between projects more easily with much shorter ramp up times, bug fixing tends to be a lot easier and making improvements on practice and process is a lot easier when those practices and processes are consistent in the first place.
In the spirit of this, the purpose of this post isn’t to tell you how you should be doing it, it’s about how we, at Amido, like to structure our own React projects. It’s an architecture we’ve settled on (for now) after a lot of trial and error, it’s based on the popular Ducks methodology, a methodology which promotes grouping by feature instead of function and it works pretty well for us – allowing us to scale our applications and on-board new team members more efficiently.
The top level of our applications, within the ‘src’ directory is comprised of 3 levels, and I’ll delve into each of these separately:
Here we keep our reusable components, they are unopinionated and can manage their own state, or be stateless. They take props and render accordingly and nothing else – any logic contained in these components should be display logic only.
These are collections of components that make up a module, this could be an entire view in an application, for example a page in React Router managed app or a reusable module that appears in multiple views. Each Composition is comprised of a React component and a Redux connector, like so:
—— └ HomePage
———— └ index.js
———— └ connect.js
Keeping our component and connector separate means each can be tested in isolation. We don’t need to test whether or not Redux is working correctly, simply whether our connector is outputting the correct props and our components are displaying correctly when receiving props. So our component will look kind of like this:
So we export the composition as a named export so that we can test it and then wrap it in our connector and export it as default for use in our wider application.
Then the connector might look a bit like this:
We keep the mapping functions separate so, again, we can test them in isolation. We would also keep the bulk of the business logic inside these connector files. If required, for larger scale applications, multiple connectors can be created for each composition and then each can be output as a named export like so:
This is really where the magic happens – we have an index file at the root level which consolidates all our reducers, or ‘ducks’, and creates and exports our store along with any middleware, such as Redux Thunk. Then each duck is a collection of action definitions, reducers and dispatches, for example:
The actions and even the reducers may contain logic, for example to map or normalise the state if it’s being retrieved from an API. They can even fire actions for other reducers to create side effects when multiple state items are affected, but generally these ducks are fairly simple and single purpose.
And that’s it, the basic Amido React / Redux application architecture. I hope you see the benefits, and feel free to use it, or don’t. No biggy.