Work in progress
An opinionated architecture for structuring large React/Redux applications.
With “App” is meant the whole JavaScript application. In create-react-app, for example, that’s thesrc
folder.
Next, think about which large logical blocks your app has/will have. Similar to Bounded Context from DDD. A package could be considered as a separate, smaller application. Even bundled separately (webpack chunks), because users of one package don’t necessarily will use another package. Examples are: User Management package, Auction package, Security package, Administration package, Reporting package, Stock package, Catalogue package…
Dividing your application into packages is only beneficial if you have a large application which you can clearly divide into logical sub-applications based on distinctly separated use cases / user roles. Otherwise, separating into modules (see below) should be enough. You could start with only modules and add the packages layer when your application grows.
Packages allow you to split a big problem into smaller problems so you can focus on particular aspects of the application, ignoring everything else.
A module represents related code of a specific concept of a package. For example the Catalogue package could have a Product module, a Cart module and an Order module . You should aim to reduce the coupling of related Modules by making the dependency between two Modules unidirectional. Every module has an index.js file which serves as the entry point for other modules. This way, a module can choose which parts it exposes in its public API.
Every package is responsible for managing the routes to the scenes inside. If you don’t have the packages layer, your routes are defined in the app layer. The Router maps routes to scenes.
Scenes are used by the Router. A scene is a React component with no data, no behaviour and no presentation. A scene groups containers and components, to create an application page.
Containers are rendered by scenes, components or other containers. A container is a React component with no data, no presentation and little or no behaviour. Containers work as the glue between services, containers and components. Containers are sometimes referred to as smart components.
Components are the smallest building blocks of the application. Scenes, containers and other components can render these components. A React component (preferably a functional component) with no data and no behaviour. Pure presentation/UI. Sometimes referred to as dumb components or presentational components.
Services are React’s Higher-Order Components (hoc). Services provide data and behaviour to containers. Encapsulating data and behaviour in a service in stead of in a container has the advantage of being very reusable inside your application. Especially if your project has both React and React Native code.
Each module should maintain their own state and actions. Every action type is prefixed with the module’s name to avoid name collisions.
// cart/actionTypes.js
export const ADD = ‘cart/ADD’;
export const REMOVE = ‘cart/REMOVE’;
In order to loosely couple your components to the state, it is a good practice to write a selectors.js file where you provide an abstraction layer for your state. A selector function receives the state and returns a (calculated) state leaf.
// cart/selectors.js
import { NAME } from ‘./constants’;
export const getCartItems = state => state[NAME].items;
Also, this allows other modules to access the cart state without having to know the exact structure.
Only Services should access the state, and pass the relevant data as props to the Containers.
Reducers and action types should be plain functions and as simple as possible. In stead, use redux-saga for your side effects and application flows.
This stackoverflow gives a good explanation of why you should prefer redux-saga over e.g. redux-thunk.
Sagas are tightly coupled to your module. They listen for certain action types and respond with new actions.
Modules should be able to control where they are mounted in the state. This way, a module selector is not coupled to the root reducer.
// rootReducer.js
import { combineReducers } from 'redux';
import cart from ‘./catalogue/modules/cart’;
export default combineReducers({
[cart.constants.NAME]: cart.reducer
});
redux-form helps reducing indeterminism caused by user input, by storing all inputs in the redux state.
Building a large application probably means having many, many interactions with a back-end service. Writing action types, actions, reducers, sagas etc for every interaction requires a lot of effort and boilerplate code. react-redux-fetch provides a good abstraction for this.