Philosophy

Building apps can be messy

It's even messier when you have to manage request lifecycles, side effects, shared global state, and 3 to 4 boilerplate files in order to manage 1 data resource. This guide aims to provide a consistent pattern intended to increase productivity and reduce entropy long term.

Typically, there are 2 common patterns implemented in redux-world that you will find in the wild:

  • Pattern A: Conventions suggested by the official redux guides
  • Pattern B: Place all redux-related files inside of the corresponding component folder

Pattern A: Official redux convention

โ”œโ”€โ”€ actions
โ”‚ โ”œโ”€โ”€ cats.js
โ”‚ โ”œโ”€โ”€ todos.js
โ”‚ โ””โ”€โ”€ users.js
โ”œโ”€โ”€ components
โ”‚ โ”œโ”€โ”€ cats
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ””โ”€โ”€ ViewAll.jsx
โ”‚ โ”œโ”€โ”€ todos
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ””โ”€โ”€ ViewAll.jsx
โ”‚ โ””โ”€โ”€ users
โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ””โ”€โ”€ ViewAll.jsx
โ”œโ”€โ”€ constants
โ”‚ โ”œโ”€โ”€ cats.js
โ”‚ โ”œโ”€โ”€ todos.js
โ”‚ โ””โ”€โ”€ users.js
โ”œโ”€โ”€ reducers
โ”‚ โ”œโ”€โ”€ cats.js
โ”‚ โ”œโ”€โ”€ index.js
โ”‚ โ”œโ”€โ”€ todos.js
โ”‚ โ””โ”€โ”€ users.js
โ”œโ”€โ”€ sagas
โ”‚ โ”œโ”€โ”€ cats.js
โ”‚ โ”œโ”€โ”€ todos.js
โ”‚ โ””โ”€โ”€ users.js
โ””โ”€โ”€ index.js

Pattern B: Component scoped convention

โ”œโ”€โ”€ components
โ”‚ โ”œโ”€โ”€ cats
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ ViewAll.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ actions.js
โ”‚ โ”‚ โ”œโ”€โ”€ constants.js
โ”‚ โ”‚ โ”œโ”€โ”€ reducer.js
โ”‚ โ”‚ โ””โ”€โ”€ sagas.js
โ”‚ โ”œโ”€โ”€ todos
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ ViewAll.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ actions.js
โ”‚ โ”‚ โ”œโ”€โ”€ constants.js
โ”‚ โ”‚ โ”œโ”€โ”€ reducer.js
โ”‚ โ”‚ โ””โ”€โ”€ sagas.js
โ”‚ โ””โ”€โ”€ users
โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”œโ”€โ”€ ViewAll.jsx
โ”‚ โ”œโ”€โ”€ actions.js
โ”‚ โ”œโ”€โ”€ constants.js
โ”‚ โ”œโ”€โ”€ reducer.js
โ”‚ โ””โ”€โ”€ sagas.js
โ””โ”€โ”€ index.js

Both ways work to organize the code and are fine. We can apply a similar structure to saga slices:

Pattern A:

- โ”œโ”€โ”€ actions
โ”œโ”€โ”€ components
โ”‚ โ”œโ”€โ”€ cats
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ””โ”€โ”€ ViewAll.jsx
โ”‚ โ”œโ”€โ”€ todos
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ””โ”€โ”€ ViewAll.jsx
โ”‚ โ””โ”€โ”€ users
โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ””โ”€โ”€ ViewAll.jsx
- โ”œโ”€โ”€ constants
- โ”œโ”€โ”€ reducers
+ โ”œโ”€โ”€ modules
+ โ”‚ โ”œโ”€โ”€ index.js
+ โ”‚ โ”œโ”€โ”€ cats.js
+ โ”‚ โ”œโ”€โ”€ todos.js
+ โ”‚ โ””โ”€โ”€ users.js
โ”œโ”€โ”€ index.js
+ โ””โ”€โ”€ store.js

Pattern B:

โ”œโ”€โ”€ components
โ”‚ โ”œโ”€โ”€ cats
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ ViewAll.jsx
- โ”‚ โ”‚ โ”œโ”€โ”€ actions.js
- โ”‚ โ”‚ โ”œโ”€โ”€ constants.js
- โ”‚ โ”‚ โ”œโ”€โ”€ reducer.js
+ โ”‚ โ”‚ โ””โ”€โ”€ sagaSlice.js
โ”‚ โ”œโ”€โ”€ todos
โ”‚ โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”‚ โ”œโ”€โ”€ ViewAll.jsx
- โ”‚ โ”‚ โ”œโ”€โ”€ actions.js
- โ”‚ โ”‚ โ”œโ”€โ”€ constants.js
- โ”‚ โ”‚ โ”œโ”€โ”€ reducer.js
+ โ”‚ โ”‚ โ””โ”€โ”€ sagaSlice.js
โ”‚ โ””โ”€โ”€ users
โ”‚ โ”œโ”€โ”€ Form.jsx
โ”‚ โ”œโ”€โ”€ ViewAll.jsx
- โ”‚ โ”œโ”€โ”€ actions.js
- โ”‚ โ”œโ”€โ”€ constants.js
- โ”‚ โ”œโ”€โ”€ reducer.js
+ โ”‚ โ””โ”€โ”€ sagaSlice.js
โ”œโ”€โ”€ index.js
+ โ”œโ”€โ”€ modules.js
+ โ””โ”€โ”€ store.js

Bringing in saga slices

In either case, it is cleaner to separate redux store, react config, and module declarations into separate files. This allows us to leave each file to perform its' intended purpose. For example:

Pattern A: ./modules/index.js

// ./modules/index.js
import CatsSlice from './cats'
import UsersSlice from './users'
import TodosSlice from './todos'
export default [
CatsSlice,
UsersSlice,
TodosSlice
];

Pattern B: ./modules.js

// ./modules.js
import CatsSlice from './cats/components/sagaSlice'
import UsersSlice from './users/components/sagaSlice'
import TodosSlice from './todos/components/sagaSlice'
export default [
CatsSlice,
UsersSlice,
TodosSlice
];

Both should only worry about importing the saga slice modules and not couple store instantiation logic inside of here. This allows for this file to be the source of truth for declaring saga slices. It can grow as much as it needs to without logic clutter.

Wiring everything up

Wiring up React, and wiring up redux can both become a stringy, tangled mess. This is why it is wise to separate the two.

File ./store.js

Should only contain redux logic. It should not be coupled with react logic at all. The helps us grow the logic needed for redux in particular away from React.

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { rootSaga, rootReducer } from 'saga-slice';
โ€‹// This is where you would bring in your array of saga slices or any other
// map of extra reducers.
import reduxModules from './modules';
import history from './utils/history';
โ€‹const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
โ€‹
export const store = createStore(
rootReducer(reduxModules, {
โ€‹
// Extra reducers
router: connectRouter(history)
}),
composeEnhancers(applyMiddleware(sagaMiddleware))
);
export const { getState, dispatch, subscribe } = store;
sagaMiddleware.run(rootSaga(reduxModules));
export default store;

File ./index.js

This file should be focused on your root level application logic relating to react. This includes any routing you might want to implement, HOCs, and containers that need to wrap your application logic.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.scss';
import App from './App';
import store from './store';
import * as serviceWorker from './serviceWorker';
export const AppWrapper = (
<Provider store={store}>
<App />
</Provider>
);
โ€‹
ReactDOM.render(AppWrapper, document.getElementById('root'));

โ€‹