Additional Guidelines For (Redux) Project Structure

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.

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 <Image> or <Title> components
that are not related to any feature per se.

Using a core module for common concerns

Using a 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.

<Todoid={123}theme="dark"size="large"onUpdate={...}/>

This hides all of the internal details of data flow within the todos module.

Contrast the above to an unconnected component.

<Todotodo={...}actions={...}theme="dark"size="large"onUpdate={...}/>

In the unconnected case, the consumer of the component has the additional responsibility of providing the queried todo object,
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.

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.

// Using sagafunction*requestProject(id){constresponse=yieldcall(fetch,`/projects/${id}`)yieldput({type:'todos/LOAD',payload:response.todos})yieldput({type:'projects/LOAD',payload:[response.project]})}// Using thunksconstrequestProject=(id)=>{returnasync(dispatch)=>{constresponse=awaitfetch(`/projects/${id}`)// Dispatch events to both todos and projects module so we can store the normalized data.dispatch({type:'todos/LOAD',payload:response.todos})dispatch({type:'projects/LOAD',payload:[response.project]})}}

If you are interested in learning more about sagas, I have a blog post about them here.

Closing

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!