Treat your actions as events not as setters or commands
Introduction:
The most common complaints about Redux is “ There is too much boilerplate ", but after seeing how most of people use Redux my conclusion is that people feel that way because they treat their actions as setters not events. If in your projects you have a 1 to 1 relationship between your actions and your reducers you are using Redux wrong.
In this article we will see how to use Redux badly (in an imperative way) and then we will fix it by using it in an event oriented way.
We will use Redux Tool Kit for the code examples.
Case Study:
To illustrate this article we will pick a simple example, given that a user authenticate himself in his expense tracker dashboard. When the user opened a modal to add an expense and validate the operation.
Then the modal is closed and the new expense is added to the expenses list.
Pretty simple isn't it ?
Imperative approach (Bad Way):
What i often see is that people dispatch multiple actions after another instead of only one action.
Doing this is a Redux antipattern, because when you do that you don't benefit from the cascading effects of your reducers.
We will see that in practice with this simple code example.
React code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const ExpenseModalContainer = () => {
const newExpense = useRef<HTMLInputElement>(null);
const dispatch: AppDispatch = useDispatch();
const modalStatus = useSelector(selectModalStatus);
return (
<>
{modalStatus === ModalStatus.AddExpense && (
<ExpenseModal
newExpense={newExpense}
onClose={() => dispatch(closeModal())}
onClick={async(e: SyntheticEvent) => {
e.preventDefault();
// the antipatern
await dispatch({ expense: newExpense });
dispatch(closeModal());
}}
title="add a new Expense"
/>
)}
// other modals...
</>
);
};
export default ExpenseModalContainer;
Redux reducers code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const expensesSlice = createSlice({
name: "expenses",
initialState,
reducers: {},
},
extraReducers: (builder) => {
builder
.addCase(addExpense.fulfilled, (state, action) => {
expensesAdapter.addOne(state.data, action.payload);
})
// other expense use cases...
});
export const modalSlice = createSlice({
name: "modal",
initialState,
reducers: {
closeModal: (state) => {
// close modal
state.status = ModalStatus.None;
}
// other modal reducers...
},
});
Event oriented approach (The Good Way):
We will reuse the code that we saw earlier in the imperative approach and fix it by removing the double dispatches (for adding a new expense and close the modal) and change the modal reducer to do all the expense adding operation + close modal operation in only one action. I also renamed all the previous actions in past tense because it is how you should always name an event.
Fixed React code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const ExpenseModalContainer = () => {
const newExpense = useRef<HTMLInputElement>(null);
const dispatch: AppDispatch = useDispatch();
const modalStatus = useSelector(selectModalStatus);
return (
<>
{modalStatus === ModalStatus.AddExpense && (
<ExpenseModal
newExpense={newExpense}
onClose={() => dispatch(modalClosed())}
onClick={async (e: SyntheticEvent) => {
e.preventDefault();
// only one action dispatched , more decoupled code the view doesn’t know too much about the flow
await dispatch(expenseAdded({ expense: newExpense }));
}}
title="add a new Expense"
/>
)}
// other modals...
</>
);
};
export default ExpenseModalContainer;
Fixed Redux code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export const expensesSlice = createSlice({
name: "expenses",
initialState,
reducers: {},
},
extraReducers: (builder) => {
builder
.addCase(expenseAdded.fulfilled, (state, action) => {
expensesAdapter.addOne(state.data, action.payload);
})
// other expense use cases...
});
export const modalSlice = createSlice({
name: "modal",
initialState,
reducers: {},
extraReducers: (builder) => {
// Will close the modal when adding an expense or when the user the click on the button for closing the modal
builder
.addMatcher(isAnyOf(expenseAdded.fulfilled,modalClosed),
(state, action) => {
// close modal
state.status = ModalStatus.None;
})
// other modal reducers...
},
});
Conclusion :
Redux is not only a global state manager , In fact i suspect that Redux choose to be a global state manager to achieve an event oriented programming style. Event though there is no code examples like the one you just saw the Redux documentation treat this topic here.
To wrap up in this article we saw why and how you should treat your actions as events and not commands or setters.