

Reduce your Redux boilerplate
source link: https://dev.to/gonzastoll/reduce-your-redux-boilerplate-17ia
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


Reduce your Redux boilerplate
Sep 6
・5 min read
Please note: This article assumes prior knowledge of how React works, how Redux works, and how the two combined work. Actions, reducers, store, all that jazz.
I’m with you on this one… creating aaall the boilerplate that’s necessary to setup your Redux store is a pain in the 🥜. It gets even worse if you have a huge store to configure, which might be the sole reason why you decide to use Redux in the first place. Over time, your store configuration can grow exponentially.
So let’s cut right to the chase. A Frontend architect (yeah, he knows stuff) recently taught me a good way to reduce
(😉) your boilerplate considerably. And it goes like this:
Store
Let’s pretend that in some part of our application we have a form where the user has to fill up some configuration data, click a button and then generate a kind of report of sorts. For that, let’s consider the following store:
// store/state.js
export const INITIAL_STATE = {
firstName: '',
lastName: '',
startDate: '',
endDate: '',
};
Actions
Now the general convention will tell you: ok, let’s create an action for each state entity to update it accordingly. That’ll lead you to do something like:
// store/actions.js
export const UPDATE_FIRST_NAME = 'UPDATE_FIRST_NAME';
export const UPDATE_LAST_NAME = 'UPDATE_LAST_NAME';
export const UPDATE_START_DATE = 'UPDATE_START_DATE';
export const UPDATE_END_DATE = 'UPDATE_END_DATE';
export const actions = {
updateFirstName(payload) {
return {
type: UPDATE_FIRST_NAME,
payload,
};
},
updateLastName(payload) {
return {
type: UPDATE_LAST_NAME,
payload,
};
},
updateStartDate(payload) {
return {
type: UPDATE_START_DATE,
payload,
};
},
updateEndDate(payload) {
return {
type: UPDATE_END_DATE,
payload,
};
},
};
You can see the boilerplate growing, right? Imagine having to add 7 more fields to the store 🤯
Reducer
That takes us to the reducer, which in this case will end up something like:
// store/reducer.js
import * as actions from './actions';
import {INITIAL_STATE} from './state';
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case actions.UPDATE_FIRST_NAME:
return {
...state,
firstName: action.payload,
};
case actions.UPDATE_LAST_NAME:
return {
...state,
lastName: action.payload,
};
case actions.UPDATE_START_DATE:
return {
...state,
startDate: action.payload,
};
case actions.UPDATE_END_DATE:
return {
...state,
endDate: action.payload,
};
default:
return {
...state,
};
}
}
Dispatch
So, now that we have our fully boilerplated store in place, we’ll have to be react accordingly and dispatch actions whenever it’s needed. That’ll look somewhat similar to:
// components/MyComponent.js
import {actions} from '../store/actions';
export default function MyComponent() {
...
const firstNameChangeHandler = value => {
dispatch(actions.updateFirstName(value));
};
const lastNameChangeHandler = value => {
dispatch(actions.updateLastName(value));
};
const startDateChangeHandler = value => {
dispatch(actions.updateStartDate(value));
};
const endDateChangeHandler = value => {
dispatch(actions.updateEndDate(value));
};
...
}
The solution
We can reduce considerably our boilerplate by creating only one action that takes care of updating the entire store. Thus reducing the amount of actions and consequently the size of the reducer.
How you may ask? By sending the entire updated entity as a payload
, and then spreading it into the state. Confused? Let's break it down.
Action
As mentioned before, only one action will be responsible for targeting the state.
// store/state.js
export const UPDATE_STORE = 'UPDATE_STORE';
export const actions = {
updateStore(entity) {
return {
type: UPDATE_STORE,
payload: {
entity,
},
};
},
};
entity
in this case makes reference to any entity located in the state. So, in our case, that could be firstName
, lastName
, startDate
or endDate
. We'll receive that entity with its corresponding updated value, and spread it in the state.
Reducer
As stated before, only one case will be fired. This case handles the updating of the state.
// store/reducer.js
import {UPDATE_STORE} from './actions';
import {INITIAL_STATE} from './state';
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case UPDATE_STORE: {
const {entity} = action.payload;
return {
...state,
...entity,
};
}
default:
return {
...state,
};
}
}
Dispatch
And finally, only one event handler with a single dispatch function:
// components/MyComponent.js
import {actions} from '../store/actions';
export default function MyComponent() {
...
// This will in turn be used as
// onClick={event => onChangeHandler('firstName', event.target.value)}
const onChangeHandler = (entity, value) => {
dispatch(actions.updateStore({[entity]: value}));
};
...
}
And with that, you’ve successfully created a store with A LOT less boilerplate, thus incrementing your productivity to focus on more important things and functionalities.
Are you a TypeScript fan as I am? Then continue reading!
TypeScript bonus!
Let’s try to puppy up this store with some TS support. We all know why TS is important. It’ll force you to write better code, makes it easy to debug by providing a richer environment for spotting common errors as you type the code instead of getting the ugly error on screen leading you to a thorough investigation of where the (most of the times) minor problem was.
So with that said, let’s get to it!
Store
If all the values are going to be empty strings by default, then we better just add them as optionals (undefined
) and only set the values on change:
// store/state.ts
export interface State {
firstName?: string;
lastName?: string;
startDate?: string;
endDate?: string;
}
const INITIAL_STATE: State = {};
Actions
We can make use of the Partial
utility type that TypeScript provides. It basically constructs a type with all the properties fed to it set to optional. This is precisely what we need, given that we'll use them conditionally.
So, create a types.ts
file where we'll define all our actions blueprints. In our case we only have the one action, but that can change with time with bigger states.
// store/types.ts
import {State} from './state';
interface UpdateStore {
type: 'store/UPDATE';
payload: {
entity: Partial<State>;
};
}
export type ActionType = UpdateStore; // union type for future actions
This file will export a Union Type constructed by all the action blueprints we've already set. Again, in our case we only have one action, but that can change with time and end up with something like:
export type ActionType = UpdateStore | UpdateAcme | UpdateFoo;
Back to the action creators, we'll again make use of the Partial
utility type.
// store/actions.ts
import {ActionType} from './types';
import {State} from './state';
export const actions = {
updateStore(entity: Partial<State>): ActionType {
return {
type: 'store/UPDATE',
payload: {
entity,
},
};
}
};
Reducer
We'll make use of the newly created Union Type containing all of our action blueprints. It's a good idea to give the reducer a return type of the State
type to avoid cases where you stray from the state design.
// store/reducer.ts
import {ActionType} from './types';
import {INITIAL_STATE, State} from './state';
export default function reducer(state = INITIAL_STATE, action: ActionType): State {
switch (action.type) {
case 'store/UPDATE': {
const {entity} = action.payload;
return {
...state,
...entity,
};
}
default:
return {
...state,
};
}
}
Dispatch
And finally, our component is ready to use all this autocompletion beauty we've already set.
// components/MyComponent.tsx
import {actions} from '../store/actions';
import {State} from '../store/state';
export default function MyComponent() {
...
const onChangeHandler = <P extends keyof State>(
entity: P,
value: State[P]
) => {
dispatch(actions.updateStore({[entity]: value}));
};
...
}
Now you have a fully flexible store, where you can add all the properties it requires without worrying about adding actions and reducer cases.
I sincerely hope this helps the same way it helped me :)
Thank you for reading!
Recommend
-
133
boilerplate with Babel, hot reloading, testing, linting and a working example app built in...
-
53
Redux is a State Mangement library for React apps as it helps to manage the app state in a single Object it means the whole app state lives on a single Object. If you try to connect a redux store you have to do s...
-
63
Boilerplate App for SaaS Product Open source web app that saves you weeks of work when building your own SaaS product. The boilerplate app comes with many basic SaaS features (see
-
52
Next.js (React) + Redux + Express REST API + Postgres SQL boilerplate Why is this awesome? This is a great starting point for a any project where you want React + Redux (with server...
-
26
React JS with Redux and Saga Project Structure A ready-to-use boilerplate for React JS with Redux and Saga. Project Overview This is a basic project structure with repeatative use cases....
-
8
Back-end6 minute readReduce Boilerplate Code With Scala Macros and Quasiquotes
-
9
Reduce boilerplate code with an automatic synthesis of Equatable and Hashable conformance Table of ContentsEquatable and Hashable are two essential protocols in the Swift world. Toda...
-
11
ICYMI C# 9 New Features: Reduce Boilerplate Constructor Code with Init Only Property SettersThis is part of a series of articles on new features introduced in C# 9. Prior t...
-
3
Photo by Roman Mager Introduction This is the first post in the series,
-
10
Boilerplate Start your new React-Native application with this React-Native boilerplate template Dec 28, 2022 2 min read
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK