0% found this document useful (0 votes)
240 views26 pages

My Awesome React Redux Structure PDF

This document discusses the author's preferred structure for React/Redux applications. It covers: 1) An overview of the recommended folder structure including sections for constants, environments, models, selectors, stores, utilities, and views. 2) Details on how to structure the application workflow with views dispatching actions and effects handling API calls before updating the store. 3) Guidelines for structuring views, stores, actions, and effects to optimize code readability, reusability, and separation of concerns.

Uploaded by

Cristhian Cruz
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
240 views26 pages

My Awesome React Redux Structure PDF

This document discusses the author's preferred structure for React/Redux applications. It covers: 1) An overview of the recommended folder structure including sections for constants, environments, models, selectors, stores, utilities, and views. 2) Details on how to structure the application workflow with views dispatching actions and effects handling API calls before updating the store. 3) Guidelines for structuring views, stores, actions, and effects to optimize code readability, reusability, and separation of concerns.

Uploaded by

Cristhian Cruz
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

My Awesome React/Redux Structure

Learn how to architect a React/Redux application in a classy way

Robert S (codeBelt) Follow


Oct 1, 2019 · 11 min read

Photo by Simon Zhu on Unsplash

I wanted to show how I structure React/Redux applications. I will be using


JavaScript ES2015 Classes for many of my files. I think classes help with
structuring your code.

If you don’t care for Classes, then check out the other source code examples:
• React/Redux (TypeScript — classes)

• React/Redux (JavaScript — classes)

• React Hooks/Redux (TypeScript — functional)

• React Hooks/Redux (JavaScript — functional)

. . .

Overview
This article will cover the following topics:

• Application workflow (best practice)

• Folder structure (awesomeness)

• Views (simplified)

• Stores (Redux)

• Actions (app communication)

• Effects (API requests)

• Reducers (data management)

• Selectors (view business logic)

See the sample application working, that corresponds to this article.


. . .

Application Workflow
I’ve worked on many applications throughout my career. From ActionScript to
the most modern JavaScript frameworks and libraries. I’ve learned a lot along
the way.

What follows are some patterns, strategies, and opinions I’ve developed along
the way.

Here is the ideal Application lifecycle:

• View dispatches an Action .

• Action calls an Effect .

• Effect handles API calls and sanitizes incoming data or returns an error.

• Action receives sanitized models or an error from the Effect and


dispatches it.

• Reducer receives the models or an error from the Action and adds them to
the store.

• Selector takes the Reducers data and creates specific data for the Views .
• View gets data straight from the store or a Selector and just displays it.
(The rest is internal communication and state management)

• Don’t mess it up!

Application lifecycle diagram

. . .

Folder Structure
I really like the below folder structure and it’s great for any size project.
React/Redux Folder Structure

• constants : Static data that doesn’t change.

• environments : API endpoints, static info that might change in different

environments.

• models : Shared models/interfaces.

• selectors : Business logic code, models/interfaces generated for views.

• stores :

- models : API related models.

- actions : Events to trigger application changes.


- effects : Handles APIs and sanitizes response data.

- reducers : Adds/removes/edits data from the global store.

• utilities : Helper code, abstract code to prevent duplication.

• views : All view components.

. . .

Views
Views should be simple and free from logic that transforms data.

All your views need to do is display data and manage themselves. Other parts of
your application can do the heavy lifting.

One thing I strive for is to “make your code read like a book”. It’s nice to start at
the top, reading left to right of a file to understand what the code is doing.
Starting at the top like a book, you can see what data the view will be using with
1 const mapStateToProps = (state, ownProps) => ({
mapStateToProps . Then, looking at componentDidMount , you will see that the view
2 someData: selectSomething(state),
will
3 dispatch a SomeAction.requestSomething
otherData: state.otherReducer.other, action.
4 });
After
5 that, we come to the important render method code and, finally, any
helper
6
methods like _onClick are placed towards the bottom of the file leaving
class SomePage extends React.Component {
7
the important code at the top of the file.
8 componentDidMount() {
9 this.props.dispatch(SomeAction.requestSomething());
Parent
10 components
} stop doing everything for your children. Let your child
components
11 get their own data and make their own action calls.
12 render() {

As13long asconst
the {components are not} going
someData, otherData to be re-used in other components, I
= this.props;
14
prefer to connect components rather than having the parent get the data and
15 return (
pass
16 in props.
<div className={styles.wrapper}>
17 <h1>{someData.header}</h1>
If18you think of components as Lego blocks, it is much easier to move them
<main>{someData.description}</main>
19
around if the<button
parentonClick={this._onClick}>{otherData.buttonLabel}</button>
is not passing data or managing callbacks for the child.
20 </div>
21 );
Looking at the component below, we can easily move <Actors /> to another
22 }
component
23 without changing any logic in the HomePage .
24 _onClick = (event) => {
25 console.log(`I was clicked`);
26class
} HomePage extends React.Component {
27 render() {
28 } return (
29
<div className={styles.wrapper}>
<MainOverview />
<Actors />
</div>
);
}
}

One thing you can do to make you child components more independent is to use
Selectors to create data that is specific to the view’s need and limiting the
amount of logic code in your views.

. . .

Stores (Redux)
In Redux, the store is a single object that keeps track of the current state of the
application. You use reducers to manage sections of the overall store .

I have a stores directory, containing folders of individual actions, effects, and


reducers files. This essentially makes up the smaller sections to manage the
single object store .

Example of folder structure:

┣━ src/
┃ ...
┃ ┗━ stores/
┃ ┣━ shows/
┃ ┃ ┣━ models/
┃ ┃ ┣━ ShowsAction.ts
┃ ┃ ┣━ ShowsEffect.ts
┃ ┃ ┗━ ShowsReducer.ts
┃ ┣━ episodes/
┃ ┃ ┣━ models/
┃ ┃ ┣━ EpisodesAction.ts
┃ ┃ ┣━ EpisodesEffect.ts
┃ ┃ ┗━ EpisodesReducer.ts
┃ ┣━ rootReducers.ts
┃ ┗━ rootStore.ts
┃ ...

As a rule of thumb, I break up my stores by API domains.

For example, if I have an API like http://api.tvmaze.com/shows/74, I would


create a ShowsAction , ShowsEffect , and ShowsReducer .

With http://api.tvmaze.com/episodes/1, I would create an EpisodesAction ,

EpisodesEffect , and EpisodesReducer .

I like doing it this way because, if a feature of that app gets removed, I can
remove a single folder rather than going to top-level folders (actions, reducers,
etc.) and remove files individually.

. . .

Actions
Actions are the core communication within your application and provide a way
to trigger side-effects and pass data to your reducers.

Flux standard action (FSA)


When it comes to actions, I follow the flux standard action pattern to determine
the structure of the action object:

1 {
2 type: 'SomeAction.ACTION_NAME', // Required
3 payload: 'anything', // Optional
4 error: false, // Optional
5 meta: null, // Optional
6 }

fsa.js hosted with by GitHub view raw


• type (required): A unique string identifier to tell consumers (reducers,
sagas, middlewares, etc.) the intent of the action.

• payload (optional): The data you want to send with the action and the
property may be any type of value.

• error (optional): If set to true , the action represents an error and the

payload should contain some sort of error data.

• meta (optional): Intended for any extra information that is not part of the
payload and the property may be any type of value. For example: an “ID”
that may not be included in the payload itself.

An action must not include properties other than type , payload , error , and

meta so all your actions are consistent and predictable.

Static classes for actions


I find static classes work well for actions. See an example below:
I find that writing your code this way makes your code self-documenting. For
example, I can clearly see that I am calling the normalAction from the
SomeAction store section:

this.props.dispatch(SomeAction.normalAction());

With the example below, it is not clear which store section of action it belongs to
and the code doesn’t read like a book:

this.props.dispatch(normalAction());

Also, when using it in the reducer, it is clear which action and action group are
used:

switch (action.type) {
case SomeAction.NORMAL_ACTION:
return {
...state,
someProperty: action.payload,
};
default:
return state;
}

Enforcing flux standard action (FSA)


To make sure everyone on your team is using the FSA in their actions let’s create
a ActionUtility and add a createAction method.
The only parameter required is the first one, which is type . The rest are set with

default values:
1 export default class SomeAction {
2
13 export default
static class ActionUtility
NORMAL_ACTION {
= 'SomeAction.NORMAL_ACTION';
24 static THUNK_ACTION = 'SomeAction.THUNK_ACTION';
35 static createAction(type, payload = undefined, error = false, meta = null) {
46 returnnormalAction()
static { type, payload,
{ error, meta };
57 } return {
68 type: SomeAction.NORMAL_ACTION,
79 } }
10 }
ActionUtility.createAction.js hosted with by GitHub view raw
11
12 static thunkAction() {
Let’s update the normalAction method to use ActionUtility.createAction .
13 return async (dispatch, getState) => {
14 const model = await SomeEffect.requestIt();
1
15 // Before
2
16 staticdispatch({
normalAction() {
3
17 returntype:
{ SomeAction.THUNK_ACTION,
4
18 type: SomeAction.NORMAL_ACTION,
payload: model
5
19 } })
6
20 } };
7
21 }
8
22 // After
9
23 static
} normalAction() {
10 return ActionUtility.createAction(SomeAction.NORMAL_ACTION);
11 }

Let’s also update the thunkAction method:


Now all our actions will be consistent.

Conventions for API request actions


I really like the following convention when it comes to managing loading and
error logic for API requests. It comes down to how your actions are named and
the convention you and your team will follow.

Any action that starts with REQUEST_ would be a starting action:

SomeAction.REQUEST_SOMETHING

Then, with an action that starts with a REQUEST_ and ends with _FINISHED , the

action would be finished:

SomeAction.REQUEST_SOMETHING_FINISHED

This way, you can create logic to determine if the loading state is either true or
false for an action. As I am using the flux standard action, mentioned above,
we can also set the error property to true/false .
This allows us to show or remove an error message when the action has finished
or restarted. You can see this implemented in the sample application.

Actions with Redux-Thunk


Take a look at the example below and you will see that I am using the action
naming convention talked about above.

Notice that, before I call ShowsEffect.requestShow(74) , I dispatch the started

action and then, once I get the data back from the request, I dispatch the
finished action.

Also notice how I set the error state. My ShowsEffect will either return a valid
model or a HttpErrorResponseModel .

1 export default class ShowsAction {


2
3 static REQUEST_SHOW = 'ShowsAction.REQUEST_SHOW';
4 static REQUEST_SHOW_FINISHED = 'ShowsAction.REQUEST_SHOW_FINISHED';
5
6 static requestShow() {
7 return async (dispatch, getState) => {
8 dispatch({type: ShowsAction.REQUEST_SHOW});
9
10 const model = await ShowsEffect.requestShow(74);
11 const isError = model instanceof HttpErrorResponseModel;
12
13 dispatch({type: ShowsAction.REQUEST_SHOW_FINISHED, payload: model, error: isError}
14 };
15 }
16
17 }

Now we can use this convention/pattern for all our request actions. One thing
we can do is abstract some of this boilerplate code to a utility. Let’s create
another method on the ActionUtility called createThunkEffect .
Let’s refactor the requestShow action method with the
ActionUtility.createThunkEffect .
Check out the full ShowsAction below from the corresponding sample code:

1 export default class ShowsAction {


2 static REQUEST_SHOW = 'ShowsAction.REQUEST_SHOW';
3 static REQUEST_SHOW_FINISHED = 'ShowsAction.REQUEST_SHOW_FINISHED';
4
5 static REQUEST_EPISODES = 'ShowsAction.REQUEST_EPISODES';
6 static REQUEST_EPISODES_FINISHED = 'ShowsAction.REQUEST_EPISODES_FINISHED';
7
8
1 //static
BeforeREQUEST_CAST = 'ShowsAction.REQUEST_CAST';
9
2 static
static REQUEST_CAST_FINISHED
thunkAction() { = 'ShowsAction.REQUEST_CAST_FINISHED';
10
3 return async (dispatch, getState) => {
11
4 static
constrequestShow()
model = await{SomeEffect.requestIt();
12
5 return async (dispatch, getState) => {
13
6 const showId = getState().shows.currentShowId;
dispatch({
14
7 type: SomeAction.THUNK_ACTION,
15
8 await ActionUtility.createThunkEffect(dispatch,
payload: model ShowsAction.REQUEST_SHOW, ShowsEffect
16
9 };
})
17
10 }
};
18
11 }
19
12 static requestEpisodes() {
20
13 return async (dispatch, getState) => {
// After
21
14 staticconst showId = getState().shows.currentShowId;
thunkAction() {
22
15 return async (dispatch, getState) => {
23
16 await
const ActionUtility.createThunkEffect(dispatch,
model = await SomeEffect.requestIt(); ShowsAction.REQUEST_EPISODES, ShowsEffect
24
17 };
25
18 } dispatch(ActionUtility.createAction(SomeAction.THUNK_ACTION, model));
26
19 };
27
20 } static requestCast() {
28 return async (dispatch, getState) => {
29 const showId = getState().shows.currentShowId;
30
31 await ActionUtility.createThunkEffect(dispatch, ShowsAction.REQUEST_CAST, ShowsEffect
32 };
33 }
34 }
In this section, I talked about Redux-Thunk but if you ever need to work with
complex logic like an ordered checkout process, I would look at adding Redux-
Saga to your application.

. . .

Effects (API Requests)


Effects are responsible for fetching data and making sure that data is sanitized
before getting passed into the application.

Let’s take a look at a snippet below of the ShowsEffect . Notice that there is an

endpoint, it makes a GET request and from the response it either returns
successful data from the API or a HttpErrorResponseModel .

1 export default class ShowsEffect {


2
3 static async requestShow(showId) {
4 const endpoint = environment.api.shows.replace(':showId', showId);
5 const response = await HttpUtility.get(endpoint);
6
7 if (response instanceof HttpErrorResponseModel) {
8 return response;
9 }
10
11 return new ShowModel(response.data);
12 }
13 }

Effects also should sanitize your data and that is what the ShowModel is doing.
Check out my article Protect Your JavaScript Applications from Api Data to learn
more.

Once again, let’s refactor and minimize the amount of boilerplate code we need
to write.
I’ve created an EffectUtility class that contains a getToModel method. The
method makes a GET request and creates a new model with the response data.

In the example below, it will create a ShowModel or return a


HttpErrorResponseModel if the request failed. This is the same as above but
cleaner.

1 export default class ShowsEffect {


2
3 static async requestShow(showId) {
4 const endpoint = environment.api.shows.replace(':showId', showId);
5
6 return EffectUtility.getToModel(ShowModel, endpoint);
7 }
8
9 }

ShowsEffect.getToModel.js hosted with by GitHub view raw

Don’t modify API data for a specific view. Pass it straight to the reducer and let
Selectors transform the API data for views.

I’ve experienced the chaos when API data has been changed for a specific view,
only to realize a few months later that the same API data is going to be used for a
completely different view.

Do yourself a favor and keep a one-to-one mapping of what the API gives you so
it is easier for everyone to view data from the original structure.

You might have noticed HttpUtility in some of the examples. I am not going to
go over it but behind the scenes it uses axios.

This allows me to transform API errors into an HttpErrorResponseModel which


helps determine if the action will have a valid payload or not. Also, it will be easy
to fix bugs or change to another library if I want to, as I only have one file to
change.
The HttpUtility is also a great place to normalize your data. Read my article
Don’t let Api Data Structure your JavaScript Application for more info.

If you were wondering what environment.api.shows is all about check out My


Awesome Custom React Environment Variables Setup.

. . .

Reducers
Reducers are smaller sections of code that manage the global store by adding,
removing, or editing data via actions.

The reducers are simple, they listen for actions to add, remove or edit data in a
section of the store.

Below, I have a static class called ShowsReducer with an initialState property


and a reduce method that adds data to the store. I want to point out that I don’t
handle error actions in the same reducer.

There is an ErrorReducer that will handle errors and you can read my article
React/Redux API Loading & Errors.
1 export default class ShowsReducer {
2 static initialState = {
3 currentShowId: '74',
Let’s
1 make
export the above
default reducer
class cleaner
ActionUtility { and faster. We are going to use inheritance
4
2 show: null,
where
5
weepisodes:
abstract[],
some of the logic and concentrate on what’s necessary.
3 static async createThunkEffect(dispatch, actionType, effect, ...args) {
6
4 actors: [],
dispatch(ActionUtility.createAction(actionType));
Below,
7
5
notice
}; how ShowsReducer extends BaseReducer . This allows us to move

the86 constmethod
reducer model = in theeffect(...args);
await above example to its own file ( BaseReducer ) so that
9 static reducer(state = ShowsReducer.initialState, action) {
not7 all reducer files have
const isError to include
= model it.HttpErrorResponseModel;
instanceof
10
8 if (action.error) {
11
9 return state;
dispatch(ActionUtility.createAction(`${actionType}_FINISHED`, model, isError));
One
12
important
}
difference from the above is that I have removed the static
10
prefix,
13
11 making
return it a normal class so it can extend the
model; BaseReducer . Notice how we

use
12 the }action
14 switch constant
(action.type)
type { as the method name:
15
13 case ShowsAction.REQUEST_SHOW_FINISHED:
16
14 staticreturn {
createAction(type, payload, error = false, meta = null) {
[ShowsAction.REQUEST_SHOW_FINISHED](state, action){}
17
15 return...state,
{ type, payload, error, meta };
18
16 } show: action.payload,
19
17 };
20
18 } case ShowsAction.REQUEST_EPISODES_FINISHED:
21 return {
22 ...state,
23 episodes: action.payload,
If you look at the above BaseReducer , you will see the reducer method.
• Line 5 : Gets access to the class method that matches the action.type .

• Line 7 : If the method is not found ( !method ) or if the action is an error


1 // Before
2
( action.error ), then it returns the current
static requestShow() {
state .

3 return async (dispatch, getState) => {


•4 Line 11 : Calls the found method with the state and action
dispatch(ActionUtility.createAction(ShowsAction.REQUEST_SHOW));
arguments
5 which will return the modified state that Redux will use.
6 const model = await ShowsEffect.requestShow(74);

rootReducer
7 const isError = model instanceof HttpErrorResponseModel;
8
Below
9
is the file that uses Redux’s combineReducers and on line 12 you can see
dispatch(ActionUtility.createAction(ShowsAction.REQUEST_SHOW, model, isError));
how
10 to set
}; up/add reducers that use BaseReducer .
11 }
12
1 import { combineReducers } from 'redux';
13 // After
2 import { connectRouter } from 'connected-react-router';
14 static requestShow() {
3 import RequestingReducer from './requesting/RequestingReducer';
15 return async (dispatch, getState) => {
4 import ErrorReducer from './error/ErrorReducer';
16 await ActionUtility.createThunkEffect(dispatch, ShowsAction.REQUEST_SHOW, ShowsEffect
5 import ShowsReducer from './shows/ShowsReducer';
17 };
6
18 }
7 export default (history) => {
 
8 const reducerMap = {
9 error: ErrorReducer.reducer, // static reducer version
10 requesting: RequestingReducer.reducer, // static reducer version
11 router: connectRouter(history),
12 shows: new ShowsReducer().reducer, // reducers that extends BaseReducer
13 };
1
14 export default class ShowsReducer extends BaseReducer {
2
15 initialState = {
return combineReducers(reducerMap);
3
16 }; currentShowId: '74',
4 show: null,
5 episodes: [],
I try
6 to keep my [],
actors: reducers simple and leave the heavy code lifting to Selectors .
7 };

If you
8 are going to need to do some extra logic in your reducers, create a helper
9 [ShowsAction.REQUEST_SHOW_FINISHED](state, action) {
method like ( _complicatedToSimple ) below, or abstract it to a utility file where it
10 return {
can
11 be reused.
...state,
12 show: action.payload,
13 }
14 }
. . .

Selectors (View Business Logic)


Selectors contain your business logic to transform store data into specific view
data so you can keep unnecessary logic out of your views and reducers.

Selectors are where you put most of your logic to generate different data
objects for your views.

I’ve even used selectors in my Thunks/Sagas to create complicated request


objects so my views dispatching the action don’t have to build up the payload in
the view.

If you look at the code below, I am using a static class to organize my logic and at
the very bottom, I have a Reselect createSelector .
I like separating the two so I can clearly see which reducers createSelector

cares about and having the static class just care about what it needs to do to the
data.

1 import { createSelector } from 'reselect';


2 import groupBy from 'lodash.groupby';
3 import dayjs from 'dayjs';
4
5 export class EpisodesSelector {
6 static selectEpisodes(episodes) {
7 const seasons = groupBy(episodes, 'season');
8
9 return Object.entries(seasons).map(([season, models]) => {
10 return {
11 title: `Season ${season}`,
12 rows: EpisodesSelector._createTableRows(models),
13 };
14 });
15 }
16
17 static _createTableRows(models) {
18 return models.map((model) => ({
19 episode: model.number,
20 name: model.name,
21 date: dayjs(model.airdate).format('MMM D, YYYY'),
22 image: model.image.medium,
23 }));
24 }
25 }
26
27 export const selectEpisodes = createSelector(
28 (state) => state.shows.episodes,
29 EpisodesSelector.selectEpisodes

If there is one thing I want to stress, it is to always try to do most of your logic in
Selectors and the rest of your app is just passing and displaying data.

Thanks for reading. Leave me a comment if you see something you liked.
I’ve covered a lot in this article but if you want to go into more details check out
my related articles:

React/Redux API Loading & Errors


I would suggest looking from the example source code but all my
actions work with “Effect” files (e.g. ShowsEffect)…
medium.com

My Awesome Custom React Environment Variables Setup


I wanted to show how I do environment variables for my JavaScript
projects. This technique is not only for Create React…
blog.usejournal.com

Improving Redux Reducers in 3 Ways


With objects, classes, and functional programming
medium.com

Protect Your JavaScript Applications from Api Data


In this article I am going to talk about one of the most important
things you can do when developing a client-side…
medium.com

Don’t let Api Data Structure your JavaScript Application


Consistency is big for me when I am creating applications. So one
standard I like to follow is to have all my code in…
medium.com

Thanks to Chris Cheney and Nate Geslin.

React JavaScript Redux Typescript Programming

About Help Legal

1 export default class ShowsReducer extends BaseReducer {


2
3 ...
4
5 [ShowsAction.REQUEST_CAST_FINISHED](state, action) {
6 const simplifiedPayload = this._complicatedToSimple(state, action.payload);
7
8 return {
9 ...state,
10 actors: simplifiedPayload,
11 };
12 }

You might also like