In this series we are looking at code organization in the context of a React and Redux application. The takeaways for the “Three Rules” presented here should be applicable to any application, not just React/Redux.
- Part 1 - Three Rules for Structuring (Redux) Applications
- Part 2 - The Anatomy Of A React & Redux Module (Applying The Three Rules)
- Part 3 - Additional Guidelines For (Redux) Project Structure
The Three Rules I presented in the first post are purposely minimal. I don’t believe in being too prescriptive on project structure, since a lot of it will depend on the type of project or personal preferences.
What I want to do in this post is to present other (hopefully helpful!) guidelines for those looking for help with code organization.
What to do with common components?
In a lot of applications, there will be common components that are used everywhere. Some examples could be
that are not related to any feature per se.
My recommendation for these components is to do one of two things.
- Keep the common components outside of the
- Make a
coremodule that includes all of these components.
Introducing a separate components folder
In this case, your application structure may be as follows.
By moving common components outside of the
modules, it may send a stronger signal that these components
are not coupled to any part of the application state.
Using a core module for common concerns
core module to group common components reduces the number of “things” in system (everything is a module!), but perhaps
at the cost of having a wider definition of what a “module” is – e.g. it may or may not manage a slice of the application state.
Regardless of whether you use a core module, or separate out common components, just make sure you don’t end up throwing too many components in there. If a component is part of a new feature then make sure a new feature module is created!
Exporting and testing connected components
A connected component in a Redux application is one that is wired up to query from the state (using selectors), and can dispatch actions.
I tend to expose connected components from my modules. The reasoning behind this is that the consumers of those components don’t necessarily want to wire up an unconnected component each time that they are used.
For example, the public interface of a
<Todo> component should be its ID, and perhaps any additional options or callbacks.
This hides all of the internal details of data flow within the
Contrast the above to an unconnected component.
In the unconnected case, the consumer of the component has the additional responsibility of providing the queried
as well as any required
actions functions (e.g. action creators). This means that you would have to keep repeating the same code
of importing action creators and selectors, and connecting them each time a
<Todo> is used!
Now, there is one downside of exporting connected components. It makes unit testing more complicated, because each time you test the component, you have to instantiate the Redux store with the correct reducers, etc.
One way to deal with this is to always test these components with stores. This has the benefit of being closer to integration tests, however, it makes testing pure renders a lot more cumbersome. That is, if you just want to test that the component renders correctly, given a set of inputs, then having to deal with stores is not ideal.
A simple solution is to export both the connected and unconnected components. The unconnected component can be exported only within the module for testing purposes.
For example, a test for
<Todo> may be as follows.
Given that the component is as follows.
And in the
todos/index.js we would only export the connected component.
Normalizing application state
The last guideline is to always normalize your application state. This makes it much more natural to work with connected components and selectors from other modules.
For example, the
todos module can hold its records in an object/map. A map is a good structure to use here since it allows easier lookup and deletion.
Then, in other modules (such as
projects), we can reference Todos by their IDs.
By normalizing data, we can avoid stale data issues, since we never duplicate the same object twice in the system. Any references to an entity has to use its ID, not an object reference or a clone.
And as noted earlier, this normalization of state plays very nicely with connected components.
Reducing to normalized states
This also means that we need to handle loading normalized data. You can either do this using a saga – where you use
put to dispatch
the appropriate loading actions – or you can use thunks.
If you are interested in learning more about sagas, I have a blog post about them here.
In this series, we began by looking at the Three Rules for structuring applications.
- Organize by features
- Create strict module boundaries
- Avoid circular dependencies
Then, in the second post, we went into in-depth examples of how to structure each module with our application.
And lastly, in this post we explored additional guidelines to help with code organization.
- Where do common/unconnected/dumb components fit in?
- Exporting and testing connected components
- Normalizing application state
I hope this series has proven useful to you. Feel free to leave a comment, or reach me on Twitter!