Skip to content

Instantly share code, notes, and snippets.

@nicolas-briemant
Last active December 23, 2020 04:44
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save nicolas-briemant/0b29950369ec385b16d4b43ca5585e62 to your computer and use it in GitHub Desktop.
Save nicolas-briemant/0b29950369ec385b16d4b43ca5585e62 to your computer and use it in GitHub Desktop.
how to structure a redux application

how to structure a redux application

A well-thought structure is helpful for the maintainability of the code.
It also helps to avoid some common anti-patterns.
A redux application is not necessary a big thing, it can also be a component that is complex enough to require redux.

There are the only 2 rules to comply with, so it is not painful to always have them in mind while developing.

use a modular organisation

In order to have a scalable application, use a modular organisation by opposition with a type organisation:

type organisation:

actions/
  spotify-search.js
  twitter-log.js
components/
  spotify-search/
    spotify-search.js
  twitter-log/
    twitter-log.js
constants/
  spotify-search.js
  twitter-log.js
reducers/
  spotify-search.js
  twitter-log.js
selectors/
  spotify-search.js
  twitter-log.js
index.js

modular organisation:

spotify-search/
  actions.js
  components/
  constants.js
  reducer.js
  selectors.js
  index.js
twitter-log/
  ...

If you want to work on a feature, you don't have to browse the entire application to get all files related to this feature.
Sometimes it won't be as obvious as it sounds.

It's the opposite of the monolithic approach and offers a way to avoid it, working on a feature should only involve 1 module folder.
If more than 1 module folder is involved, either the code smells or the task is too big.

The modular organisation also eases the following rule.

define an API for each module

The complexity of an application can have many reasons, one of them is the interleaving of modules (which is commonly called the spaghetti code).
In other words, coupling things together make it complicated to reason about 1 specific thing and not be forced to deal about all other things.
A simple task become quickly a hard one as the application grows.

bad (interleaving):

// in twitter-log
import reducer from '../spotify-search';
import SpotifyView from '../spotify-search/components/spotify-search-view';

This code implies that within the twitter-log module, i know how the spotify-search module is made.
If spotify-search is updated, then you have to look for all dependent modules and update them accordingly.
Interleaving is a structural coupling, coupling is evil.

good (rely on module API):

// in twitter-log
import spotifySearch from '../spotify-search';
const { reducer, SpotifyView } = spotifySearch;

Decoupling modules allows to freely update them without breaking dependent modules, as soon as the API is not changed.
Instead of reaching inside other modules, we define and maintain contracts between them.

The last thing to know is how and where I define the API of a module:

// in index.js

// the module knows its structure
import reducer from './reducer';
import SpotifyView from './components/spotify-search-view';

// public API of the module
export { reducer, SpotifyView };

sample structure

spotify-search/
  components/
  actions.js
  action-types.js
  constants.js
  index.js
  model.js
  reducer.js
  selectors.js
index.js
root-reducer.js
  • filenames are using the spinal-case convention (lowercase, hyphen separator).

index.js

import * as actions from './actions';
import * as components from './components';
import * as constants from './constants';
import reducer from './reducer';
import * as selectors from './selectors';

export default { actions, components, constants, reducer, selectors };

contants.js

export const NAME = 'spotify-search';

action-types.js

import { NAME } from './constants';

export const SEARCH = `${NAME}/SEARCH`;
  • using NAME is action types avoid name collisions with other modules of an application.

actions.js

import * as types from './action-types';

export const search = (text) => ({
  type: types.SEARCH,
  payload: { text }
});
  • actions are action creators (redux world)
  • use payload instead of directly text, it allows to have a standard action interface (useful in middlewares)

reducer.js

Regarding reducers, there is a coupling issue within a redux application because the store is a singleton.
This implies module reducers to be mounted regarding the need of an application.
But module reducers are not aware of their possible usage, if so it means that they are tied to a specific application.

In order to break this natural coupling, a solution is to let the module tell the world how to mount it:

// in root-reducer.js
import { combineReducers } from 'redux';
import spotifySearch from './spotify-search';

export default combineReducers({
  [spotifySearch.constants.NAME]: spotifySearch.reducer
});
// in reducer.js
import * as types from './action-types';

const initialState: {
  text: 'Royksopp'
};

export default (state=initialState, action={}) => {
  switch (action.type) {
    case types.SEARCH:
      return [
        // ...
      ];
    // ...
  }
};
  • initial state is defined inside reducer.js

selectors.js

Selectors are used as a query language for the module, where the state is the database.

import { createSelector } from 'reselect';
import { size } from 'lodash';
import { NAME } from './constants';

export const getState = state => state[NAME];
export const getSearchResults = compose(state => state.results, getState);

export const getSearchResultCount = createSelector(
  getState, getSearchResults,
  (allTodos, searchResults) => ({
    searchResultCount: size(searchResults)
  })
);
  • getState allows to reach the subtree related to the spotify search module

components

Components contains React views and Containers that connect views to the state through selectors.
Views should as dumb as possible, they should describe the UI according to a given state.

import { createStructuredSelector } from 'reselect';
import { getSearchResults } from '../selectors';
import ResultView from './result-view';

const ResultsView = ({ results }) => (
  <div>
    results.map(result => <ResultView result={ result } />)
  </div>
);

export default connect(
  createStructuredSelector({
    results: getSearchResults
  })
)(ResultsView);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment