27
Event Driven Architecture for Redux
This article elaborates one of the Redux best practices. This best practice has helped me a ton in writing better code and avoiding problems. Many thanks to the Redux team!
Redux provides powerful way to store state data for client-side applications. Any part of application can send data, through actions, to be stored in redux then data becomes available to entire application.
However, with great power comes great responsibilities! Poorly designed actions/reducers give away most of redux advantages, and application becomes difficult to understand and debug. While well designed actions/reducers helps in keeping the store logic maintainable and scalable.
We have been told that storage is cheap. However, we still cannot store everything. Additionally, memory is actually very expensive.
While designing data storage systems we need to be mindful of what data is worth storing to solve our problems. Generally relevant events result in creation of valuable data. While absence of events does not generate any useful data.
Example: There is no point in keep recording footages of a football stadium when no game is being played.
Similarly, in the world of client-side applications. Useful data, which is required throughout a session of application, is generated when events (user, Web-API, Web-Sockets, etc.) occur. Therefore, designing state tracking system based out of important events result in more maintainable, intuitive, and scalable system.
This a very prevalent approach in many redux applications. Developers create many actions to set state in redux store.
This architecture results in following problems:
- Developers need to be careful while designing event handlers and split payload properly as per setter actions.
- Dispatch many actions when important events occur. In turn overwhelming actions log, which makes time travel debugging difficult.
- Number of actions explodes when more and more data is required to be stored from same event.
- Due to developer oversight, residual actions can keep lingering in codebase when data requirements reduce.
Objective: For a food ordering application:
- Customer can order any number of pizzas
- Customer can order any number of cokes
- Once customer selection is complete order is sent (main event)
For setter actions: reducer logic look like (1) and action creators like (2)
const orderSlice = createSlice({
name: "order",
initialState: { pizzaOrdered: 0, cokeOrdered: 0 },
reducers: { // (1) Reducer logic
setPizzaOrdered: (state, action) => {
state.pizzaOrdered = action.payload;
},
setCokeOrdered: (state, action) => {
state.cokeOrdered = action.payload;
}
}
});
const {
actions: { setPizzaOrdered, setCokeOrdered }, // (2) Action creators
reducer: orderReducer
} = orderSlice;
Send order event handler looks like (3)
const sendOrder = () => { // (3) Send Order event handler
dispatch(setPizzaOrdered(pizza));
dispatch(setCokeOrdered(coke));
};
(3) is another bad practice
And action log looks like (4)
In sizable application setters action log explodes
Problem 1: Adding fries to menu
- New setter action/reducer (setFriesOrdered) needs to be created in (1) and (2).
- One more action needs to be dispatched in (3), send order event.
- (4) Action log will increase to show one more action order/setFriesOrdered.
Problem 2: Removing coke from menu
- Setter action/reducer (setCokeOrdered) should be deleted in (1) and (2). However, this deletion is not necessary. Therefore, developers have tendency to miss out deletion. Also, in large teams they hesitate, thinking someone else might be using this action. Resulting in bloated codebase.
- setCokeOrdered action needs to be removed in (3). Application needs to be aware of changing actions. All the imports need to be adequately removed.
Deriving actions/reducers based out of application events improves the design of redux store significantly. Primarily, due to the fact that data worth storing originates from events.
This architecture has following advantages:
- Dispatch only one action per event, resulting in intuitive separation of concerns between application code and redux code.
- To store more data from an event: developers need to increase payload for the existing action, while reducer manages the internal state of redux.
- To store lesser data from an event: developers need to decrease payload for the existing action, while reducer manages the internal state of redux. No need to manage actions.
Play around with code here.
For food order example: reducer logic looks like (5) and action creator looks like (6)
const orderSlice = createSlice({
name: "order",
initialState: { pizzaOrdered: 0, cokeOrdered: 0 },
reducers: { // (5) Reducer logic
orderPlaced: (state, action) => {
state.pizzaOrdered = action.payload.pizza;
state.cokeOrdered = action.payload.coke;
}
}
});
const {
actions: { orderPlaced }, // (6) Action creator
reducer: orderReducer
} = orderSlice;
Send order event handler looks like (7)
const sendOrder = () => { // (7) Send Order event handler
dispatch(orderPlaced({pizza, coke}));
};
And action log looks like (8)
Problem 1: Adding fries to menu
- orderPlaced reducer in (5) needs to be adjusted per expanded requirement.
- Payload in (7) needs to increase with fries information.
- Action log remains same!
Problem 2: Removing coke from menu
- orderPlaced reducer in (5) needs to be adjusted per reduced requirement.
- Payload in (7) needs to remove coke's information.
- Action log remains same!
When I started using redux I used to create setter type actions. Upon reading this best practice I had following apprehensions:
- Setters provide me with granular access to redux state and I can dispatch as many actions from wherever in the application per my needs.
Resolution: Actions are only required when events, important to the application, occur.
- Setters give me flexibility in adding/removing actions from event handlers as per my needs.
Resolution: Setters are actually tedious because you need to import actions, add them in event handlers and update reducers per changing logic. Managing state changes in event based reducer is easier because you only need to adjust reducer logic and payload.
27