Quantcast
Channel: Jack Hsu
Viewing all articles
Browse latest Browse all 38

The Anatomy Of A React & Redux Module (Applying The Three Rules)

$
0
0

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.

Series contents


In this post, we are expanding on the three rules of structuring applications and diving deeper into the contents of the different files with a Redux and React application.

This post will be specifically about React and Redux, so feel free to skip to the next post if you are not interested in these libraries.

In-depth example and recommendations

Recall the three rules presented in the previous post:

  1. Organize by features
  2. Create strict module boundaries
  3. Avoid circular dependencies

Now, let’s take a look at our TODO app again. (I added constants, model, and selectors into this example)

todos/components/actions.jsactionTypes.jsconstants.jsindex.jsmodel.jsreducer.jsselectors.jsindex.jsrootReducer.js

We can break these modules down by their responsibilities.

Module index and constants

The module index is responsible for maintaining its public API. This is the exposed surface where modules can interface with each other.

A minimum Redux + React application should be something like this.

// todos/constants.js// This will be used later in our root reducer and selectorsexportconstNAME='todos';
// todos/index.jsimport*asactionsfrom'./actions';import*ascomponentsfrom'./components';import*asconstantsfrom'./constants';importreducerfrom'./reducer';import*asselectorsfrom'./selectors';exportdefault{actions,components,constants,reducer,selectors};
Note: This is similar to theDucks structure.

Actions & Action creators

Action types are just string constants within Redux. The only thing I’ve changed here is that I prefixed each type with “todos/” in order to create a namespace for the module. This helps to avoid name collisions with other modules in the application.

// todos/actionTypes.jsexportconstADD='todos/ADD';exportconstDELETE='todos/DELETE';exportconstEDIT='todos/EDIT';exportconstCOMPLETE='todos/COMPLETE';exportconstCOMPLETE_ALL='todos/COMPLETE_ALL';;exportconstCLEAR_COMPLETED='todos/CLEAR_COMPLETED';

As for action creators, not much changes from the usual Redux application.

// todos/actions.jsimport*astfrom'./actionTypes';exportconstadd=(text)=>({type:t.ADD,payload:{text}});// ...

Note that I don’t necessarily need to use addTodo since I’m already in the todos module. In other modules I may use an action creator as follows.

importtodosfrom'todos';// ...todos.actions.add('Do that thing');

Model

The model.js file is where I like to keep things that are related to the module’s state.

This is especially useful if you are using TypeScript or Flow.

// todos/model.jsexporttypeTodo={id?:number;text:string;completed:boolean;};// This is the model of our module state (e.g. return type of the reducer)exporttypeState=Todo[];// Some utility functions that operates on our modelexportconstfilterCompleted=todos=>todos.filter(t=>t.completed);exportconstfilterActive=todos=>todos.filter(t=>!t.completed);

Reducers

For the reducers, each module should maintain their own state as before. However, there is one particular coupling that should be solved. That is, a module’s reducer does not usually get to choose where it is mounted in the overall application state atom.

This is problematic, because it means our module selectors (which we will cover next) will beindirectly coupled to the root reducer. In turn, the module components will also be coupled to the root reducer.

We can solve this issue by giving control to the todos module on where it should be mounted in the state atom.

// rootReducer.jsimport{combineReducers}from'redux';importtodosfrom'./todos';exportdefaultcombineReducers({[todos.constants.NAME]:todos.reducer});

This removes the coupling between our todos module and root reducer. Of course, you don’t have to do it this way. Other options include relying on naming conventions (e.g. todos module state is mounted under “todos” key in the state atom), or you can use module factory functions instead of relying on a static key.

And the reducer would look as follows.

// todos/reducer.jsimporttfrom'./actionTypes';importtype{State}from'./model';constinitialState:State=[{text:'Use Redux',completed:false,id:0}];export(state=initialState,action:any):State=>{switch(action.type){caset.ADD:return[// ...];// ...}};

Selectors

Selectors provide a way to query data from the module state. While they are not normally named as such in a Redux project, they are always present.

The first argument of connect is a selector in that it selects values out of the state atom, and returns an object representing a component’s props.

I would urge that common selectors by placed in the selectors.js file so they can not only be reused within the module, but potentially be used by other modules in the application.

I highly recommend that you check out reselect as it provides a way to build composable selectors that are automatically memoized.

// todos/selectors.jsimport{createSelector}from'reselect';import_from'lodash';import{NAME}from'./constants';import{filterActive,filterCompleted}from'./model';exportconstgetAll=state=>state[NAME];exportconstgetCompleted=_.compose(filterCompleted,getAll);exportconstgetActive=_.compose(filterActive,getAll);exportconstgetCounts=createSelector(getAll,getCompleted,getActive,(allTodos,completedTodos,activeTodos)=>({all:allTodos.length,completed:completedTodos.length,active:activeTodos.length}));

Components

And lastly, we have our React components. I encourage you to use shared selectors here as much as possible. It gives you the advantage of having an easy way to unit test the mapping of state to props without relying on component tests.

Here’s an example of a TODO list component.

import{createStructuredSelector}from'reselect';import{getAll}from'../selectors';importTodoItemfrom'./TodoItem';constTodoList=({todos})=>(<div>todos.map(t=><TodoItemtodo={t}/>)</div>);exportdefaultconnect(createStructuredSelector({todos:getAll}))(TodoList);

Other responsibilities

There are many other things that could be included in a module. For example, you may be using redux-saga, which should definitely be included in your module structure – where each module can optionally expose a root saga.

Other things that you may include are route handlers (or containers) for use with react-router– or a similar routing library.

Summary

We’ve seen in this post how to break down a React/Redux module into individual core responsibilities.

  • Index and constants - The public API and constants.
  • Actions and action creators - Information that flow through the application.
  • Model - Types and utilities for the model.
  • Reducers - Updates module state.
  • Selectors - Queries module state.

In the last post of this series on code organization, we will discuss additional guidelines that fall outside of the “Three Rules.”


Viewing all articles
Browse latest Browse all 38

Trending Articles