TLDR; You can use redux without a lot of boilerplate with just one action changing the whole reducer state. Consistency of the structure can be reached with immutable-helper and typescript.
For establishing this work-flow you will need:
Okay so you probably know redux and what it is used for. A global state - one source of truth. When it is changed, appropriate components get rerendered with a new data from the store. This is used mainly to avoid passing handlers and data back and forth between components and their parents.
There are an actions, reducers and the store. React components should dispatch actions to store. Then redux finds appropriate reducer to handle the action by its type. Then new state is returned from reducer and store will notify all components to rerender.
If you have ever worked with redux you have always end up with a structure similar to this.
Every action has its own case handler inside switch in a reducer. Like so:
const reducer = (state = defaultState, action) => {
switch (action.type) {
case ActionType.LOAD_POSTS_REQUEST:
return {
...state,
posts: action.payload,
}
case ActionType.LOAD_POSTS_REQUST_SUCC:
return {
...state,
somethings,
}
case ...:
return {
...state,
somethings,
}
default:
return state;
}
}
Sometimes, if your state is a little bit nested and if you want to change one attribute inside nested structure, you end up doing this maybe:
case ActionType.UPDATE_SOMETHING_NESTED:
return {
...state,
some_attr: {
...state.some_attr,
another_nested: {
...state.some_attr.another_nested,
a: 3,
}
}
}
You can’t just do state.some_attr.another_nested.a = 3;
, you want to keep your data immutable and you want to have different reference for the structures, that has changed. As you can see, if you do immutability here with vanilla options, your code starts to looks like the one above or even worse. Trust me, you don’t want this. We get to this problem later with our immutability-helper
A lot of boilerplate code for me. I didn’t like this approach from the first moment I have started using it. A code always ended up with a few hundred of lines and I started to get lost inside my own code.
A good practice is also keeping a file named ActionTypes
where you have your action types stored and then you would use them over your actions and reducers as variables.
export const FETCH_ALL_POSTS = "FETCH_ALL_POSTS";
export const FETCH_ALL_POSTS_SUCC = "FETCH_ALL_POSTS_SUCC";
export const FETCH_ALL_POSTS_ERR = "FETCH_ALL_POSTS_ERR";
// ... and so on and so on
What I ended up was a big unnecessary file with a lot of these types. Do you know where I did a lot of bugs? When CTRL+C and CTRL+V these types and forget to change a string. So I ended up searching for bug for a next 30 minutes.
// EASY TO MAKE A MISTAKE
// SEE THE THIRD FETCH_ALL_POSTS_ERR
export const FETCH_ALL_POSTS = "FETCH_ALL_POSTS";
export const FETCH_ALL_POSTS_SUCC = "FETCH_ALL_POSTS_SUCC";
export const FETCH_ALL_POSTS_ERR = "FETCH_ALL_POSTS_SUCC"; // <-- DO YOU SEE THAT SUCC ??? :)
Then there is so called action creators. Action creators are functions, that create an object (Action) for you: Such as this:
// Again copying all
const fetchAllPostsActionCreator = () => ({
type: ActionTypes.FETCH_ALL_POSTS
});
const fetchAllPostsSuccActionCreator = (payload) => ({
type: ActionTypes.FETCH_ALL_POSTS_SUCC,
payload,
});
const fetchAllPostsErrActionCreator = (error) => ({
type: ActionTypes.FETCH_ALL_POSTS_ERR,
error,
});
Do you like that long function namings? I don’t like them. We use action creators because we do not want to create an action objects by hand - so we can’t forget what ActionType has to go there or what name of payload and other attributes and so on. The thought behind it is OK.
But, do you see how many code is being generated, and we didn’t do anything yet. But we do it for safety and consistency, testability and other things some manager/team leader wants right?
So disadvantages for me was:
Have only one reducer switch case and set state from an action.
I strongly recommend using Typescript here.
// actions.ts
const setPostsState = (newState: PostsState) => store.dispatch({
action: "SET_POSTS_STATE", // Only one action type for this reducer
payload: newState,
});
Noticed how I wrapped this function with store.dispatch
? I did it because I wanted an action to be really the action in the real sense of meaning. When I want to setPostsState, I really want to set the fu*king state. I don’t want action type, I don’t want action creator, I don’t want an object… I just want to set store state and move on with things.
With this, I don’t have to wrap it like this store.dispatch(SetPostsStateAction)
… I ended up in a lot of cases where my store just didn’t updated because I called action creator without dispatch and so on. So I kept it simple and had dispatch function included in this one. Simple.
// reducer.ts
const defaultState: PostsState = {
posts: [],
fetch: {
error: null,
loading: false,
}
};
const postsReducer = (state: PostsState = defaultState, action: PostsActions) => {
switch (action.type) {
case "SET_POSTS_STATE":
return action.payload;
default:
return state;
}
}
Advantages:
Immutability-helper is package that can updates nested structure. When structure is updated a new immutable object with a new reference is returned. Only structures that really have changed have different reference. Then redux can easily does a reference comparison and knows whether something has changed or not. Syntax is at the beggining a little weird, but when you get used to, you will not want to set immutable state differently.
import update from "immutability-helper";
const { postsState } = store.getState();
newState = update(newState, {
fetch: {
loading: {
$set: true,
}
},
});
// now we can use our set state function
setPostsState(newState);
One more thing: You can do this safely from anywhere from your code. One disadvantage is, that when type of the posts state will change, you need eventually update every access setting point in your app. If for some reason will be loading inside state object renamed to loadingRequest, you need to update it everywhere and typescript doesn’t let you compile your code.
You can’t see a real advantage here because our state is too small. But when your state starts to have more attributes and these attributes are nested. Trust me, you won’t heard a word against it.
If you see some misunderstaning, error, mistake, grammar mistake or just have a question then I would be more than pleased if you would write a comment below. Thank you.
In the last part How To Use Redux Without Losing Your Mind I’ve presented the new way of using redux libary. The main reason behind…
I see people always struggling with include/exclude notation inside use case diagrams. I think these diagrams could explain it better. use…
Lucinda is a lazy black fluffy girl. Her name was generated by random name generator and I’m not joking now. Lucinda looks somewhat like…