My Redux Pattern
Folder Structure
There are a number of ways to set up your folder structure for redux, with the most basic simply being a folder for ActionTypes, Actions, Reducers and Selectors.
However, as your data model gets more complex these files get crowded. I initially tried creating a separate filename for each node in my data model under the Actions, Reducers, etc folders, but you find yourself hopping around a lot.
Then I found the "Ducks" pattern, whose goal is to encapsulate much of this in a single file for each area of functionality.
I opted for a modified version of ducks based on this article: Scaling Your Redux App With Ducks
Here is the original ducks proposal
Below is a screenshot of what my folder structure looks like:
You can see most redux functionality is in the store folder.
configureStore.js
At the root we have the configureStore.js file. This will do our initial setup.
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { AUTH_LOGGED_OUT, startLogin } from './auth';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
// *** We are pulling the reducers as defaults from the index files in each directory
import tvShowReducer from './tvShows';
import authReducer from './auth';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const initialState = {
TV: {
showData: {},
seasonData: {},
extraData: {}
},
auth: {
uid: undefined,
status: AUTH_LOGGED_OUT,
msg: undefined
}
}
export default () => {
//store creation
const store = createStore(
combineReducers({
TV: tvShowReducer,
auth: authReducer,
}),
initialState,
composeEnhancers(applyMiddleware(thunk, logger))
);
return store;
};
Each of the folders under store holds all redux activity for a node in the data model.
So the tvShows folder exports the reducer for the TV: node.
Let's go through each file in the sub folders.
Here is another way to setup the configureStore() function. This returns a function that will be imported AND executed to setup the store.
import { createStore, applyMiddleware, combineReducers, compose } from "redux";
import thunk from "redux-thunk";
import variableEditorReducer from "./variableEditor/reducer";
import appStateReducer from "./appState/reducer";
export default function configureStore() {
// combine reducers
const reducer = combineReducers({
appState: appStateReducer,
variableEditor: variableEditorReducer
});
// Middleware to only be used in development
const devMiddleware =
process.env.NODE_ENV !== "production"
? [require("redux-immutable-state-invariant").default()]
: null;
// If you don't need to use redux dev tools
//return createStore(rootReducer, applyMiddleware(thunk));
let composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
return createStore(
reducer,
composeEnhancers(applyMiddleware(...devMiddleware, thunk))
);
}
Actions.js
This file will hold the Action Type constants and all of the Action Creators. These will all be named exports:
export const LOGIN = 'LOGIN'
export const startLogin = () => {}
Reducers.js
This file will hold all of the reducers. You can also use combineReducers in this file if there are sub nodes in the data model.
For example our data shape is
{
TV: {
showData: {},
seasonData: {}
}
}
With the above, in createStore we would do this:
import tvShowReducer from './tvShows';
export default () => {
//store creation
const store = createStore(
combineReducers({
TV: tvShowReducer, // <----
auth: authReducer,
}),
initialState,
composeEnhancers(applyMiddleware(thunk, logger))
);
return store;
};
But then in our Reducers file we would have created two separate reducer functions and used combineReducers on them before exporting.
import { combineReducers } from 'redux';
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// - showData reducer
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
const showDataDefault = {};
const showDataReducer = (state = showDataDefault, action) => {
switch (action.type) {
case ADD_TV_SHOW:
return ...
case DELETE_TV_SHOW:
return ...
}
default:
return state;
}
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// - seasonData reducer
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
const seasonDataDefault = {};
const seasonDataReducer = (state = seasonDataDefault, action) => {
switch (action.type) {
case ADD_TV_SHOW:
return ...
case DELETE_TV_SHOW:
return ...
}
default:
return state;
}
};
const reducer = combineReducers({
showData: showDataReducer,
seasonData: seasonDataReducer,
});
export default reducer;
We will always export default reducer
Reducer Helper / Utility Functions
This is not a must, but could clean up some code for common things that you do in your reducers.
As you write your reducers, be on the lookout for common patterns and place those in utility functions.
One example from Udemy - React Course is for the patter of returning a new object based on state with updates. In your reducer it may look like this:
...
switch(action.type) {
case "SOME_ACTION":
return {
...state,
someProp: action.payload
}
...
default:
return state;
}
...
You can encapsulate that logic into an Utility function called updateObject.
function updateObject(state, updates) {
return {
...state,
...updates
}
}
// Now in your reducer
...
switch(action.type) {
case "SOME_ACTION":
return updateObject(state, {someProp: action.payload})
...
default:
return state;
}
...
selectors.js
Selectors.js will contain all of the functions we need to slice and dice the data for this node.
All of these are also named exports.
For details
index.js
This file is very important as it allows us to import all of these functions and constants easily. It will look the same for every functional directory:
import reducer from './reducers';
export * from './actions';
export * from './selectors';
export default reducer;
Here is how you would import and access your reducers, actions and selectors.
import tvShowReducer from './store/tvShows' //<-- default export so just access directory
import { ACTION_TYPE } from './store/tvShows' //<-- Named export accessed by name
import { startAddTVShow } from './store/tvShows'//<-- Named export accessed by name
import { selectSomeData } from './store/tvShows'//<-- Named export accessed by name