Redux is the most popular state management library in JavaScript ecosystem for Single Page Applications. However, probably it would be much more popular if not infamous statements, like Redux is verbose, Redux boilerplate and so on. In my opinion though, there is only one part of Redux which could be easier to use, namely Redux actions. In this article I will try to point some issues with Redux actions and what we could do to mitigate them.
Not necessarily verbose parts in Redux
Before we begin, let’s talk about two things which could be considered as verbose, but in my view are not.
Separate actions and reducers
There are many complains that in Redux you need to write actions and reducers separately. For me this is a good thing and actually this was done by design. We shouldn’t think that actions and reducers have 1 to 1 relationship. One reducer can react to many separate actions… and many reducers can react to the very same action. This is one of the most powerful features of Redux, often not appreciated.
Switch statements in reducers
Many of us hate switch
statements in reducers. This is opinionated though and there are many libraries which allow to write reducers in different ways. We will write such a helper a little later in this article too!
Truly verbose parts in Redux
For me, the most problematic parts of Redux are related to actions, constants and thunks. What’s more, those problems are not only about verbosity, but also about potential bugs, like types collision. Let’s name those issues and try to fix them one by one, until there is nothing left!
Constants
In my head, this was always the most annoying thing in Redux. Writing separate actions and constants is not only verbose, but also error-prone. Moreover, it also introduces some disorder to our imports. For example, you need constants to recognize actions, but you need actions (action creators to be precise, but let me stick with actions shortcut for simplicity) to be able to dispatch them. Often you end up importing an action and a constant related to the very same action! What if we could give up constants altogether without any compromise? Let’s try to write a helper function!
const createAction = (name, action = () => ({})) => {
const actionCreator = (...params) => ({
type: name,
...action(...params),
});
actionCreator.toString = () => name;
return actionCreator;
};
So, what we just did? Instead of explaining, let’s just try to use it! Imagine we have an action like that:
const INCREMENT_BY_VALUE = 'INCREMENT_BY_VALUE';
const incrementByValue = value => ({
type: INCREMENT_BY_VALUE,
value,
)};
We could rewrite it like that now:
const incrementByValue = createAction(
'INCREMENT_BY_VALUE',
value => ({ value }),
);
As you can see, we pass INCREMENT_BY_VALUE
type as the 1st argument to createAction
, which does the rest job for us. But wait a second, we don’t have constants anymore, so how we could use it in reducers for example? The key is actionCreator.toString = () => name
line in createAction
body, which allows us to get action type constant like incrementByValue.toString()
. So, the action is the source of its type at the same time, so no more keeping constants and actions in sync, you need just actions and you are done! As a bonus, sometimes you won’t even need to call toString()
manually, see how in the next paragraph!
Avoiding manual toString
calls in reducers
Before we solve this issue, see how a reducer reacting to incrementByValue
action could look like:
const valueReducer = (state = 0, action) => {
switch (action.type) {
case incrementByValue.toString():
return state + action.value;
default:
return state;
}
};
It uses the standard switch
statement, which some people love and some people hate, the only issue in comparison to normal reducers is this nasty incrementByValue.toString()
, which is needed to get the proper INCREMENT_BY_VALUE
type. Fortunately for switch
and toString
haters, there is a solution, let’s create a reducer helper function:
const createReducer = (handlers, defaultState) => {
return (state, action) => {
if (state === undefined) {
return defaultState;
}
const handler = handlers[action.type];
if (handler) {
return handler(state, action);
}
return state;
};
};
Now, we could refactor valueReducer
as:
const valueReducer = createReducer({
[incrementByValue]: (state, action) => state + action.value,
}, 0);
As you can see, no switch
or toString
anymore! Because we replaced switch
with handlers
object, we can use computed property [incrementByValue]
, which calls toString
automatically!
Thunks
For many developers thunks are used to create side-effects, often as an alternative to redux-saga
library. For me they are something more though. Often I need an argument in my actions, but such an argument which is already present in Redux store. Again, there are many opinions about this, but for me passing to action something already present in the store is an antipattern. Why? Imagine you use Redux with React and you dispatch an action from React. Imagine that this action needs to be passed something already kept in the store. What would you do? You would read this value by useSelector
, connect
or something similar first, just to pass it to the action. Often this component wouldn’t even need to do that, because this value could be only action’s dependency, not React component’s directly! If Redux action could read the state directly, this React component could be much simpler! So… thunks to the rescue! Let’s write one!
const incrementStoredValueByOne = () => (dispatch, getState) => {
const { value } = getState(); // we could use selector here
return dispatch({
type: 'INCREMENT_STORED_VALUE_BY_ONE',
newValue: value + 1,
});
};
Before we continue, of course this example might to too naive, we could solve this problem by a proper logic in reducer, it is just to illustrate the problem. Anyway, notice, that this thunk reads current value from the store instead of getting it as an argument. Problem solved then! Not so quick! Again, what about types? If you need to refactor an action to thunk just to read state from Redux directly, you will end up with the constants issue we already solved by createAction
again. So what should we do? Do something similar but just for thunks!
const createThunk = (name, thunk) => {
const thunkCreator = (...params) => (dispatch, getState) => {
const actionToDispatch = thunk(...params)(dispatch, getState);
return dispatch({ type: name, ...actionToDispatch });
};
thunkCreator.toString = () => name;
return thunkCreator;
};
Now, we could refactor our thunk like that:
const incrementStoredValueByOne = createThunk(
'INCREMENT_STORED_VALUE_BY_ONE',
() => (dispatch, getState) => {
const { value } = getState(); // we could use selector here
return { newValue: value + 1 };
},
};
Again, no constants! incrementStoredValueByOne.toString()
will return INCREMENT_STORED_VALUE_BY_ONE
, so you could even listen to this thunk in your reducers directly!
Other problems
We solved many issues already, but unfortunately there are more:
- You still need to pass action type in
createAction
orcreateThunk
as the first argument, which is kind of duplication. It would be cool if we could define actions likeconst myAction = createAction()
instead ofconst myAction = createAction('MY_ACTION')
- What about the risk of action types collision? What if 2 of your actions will have the very same name? The bigger the application, the bigger chance this could happen. There are already libraries, which try to fix that, for example by adding a counter to types. However, those solutions are not deterministic, which will cause troubles with Hot Module Replacement and possibly Server Side Rendering.
-
createAction
andcreateThunk
should have some Typescipt types, otherwise you won’t get proper autocomplete in a text editor like Visual Studio Code. - Should we really care about those things during writing applications? We should have a ready to use solution!
Fortunately, now such a solution exists…
Introducing redux-smart-actions
library
Let me introduce redux-smart-actions library, the fastest way to write Redux actions!
This library provides all the utilities like createAction
, createThunk
, createReducer
, and at the same time solves all mentioned issues not covered in this article. Points 1 and 2 are solved by the optional babel-plugin-redux-smart-actions
. Point 3 is solved as Typescript types are included in the library. And point 4… is solved by any library anyway, including this one ;)
Basically with its help you could transform your code like that:
+ import {
+ createSmartAction,
+ createSmartThunk,
+ createReducer,
+ joinTypes,
+ } from 'redux-smart-actions';
+
- const RESET_VALUE = 'RESET_VALUE';
- const SET_VALUE = 'SET_VALUE';
- const INCREMENT_IF_POSITIVE = 'INCREMENT_IF_POSITIVE';
-
- const resetValue = () => ({ type: RESET_VALUE });
+ const resetValue = createSmartAction();
- const setValue = value => ({ type: SET_VALUE, value });
+ const setValue = createSmartAction(value => ({ value }));
- const incrementIfPositive = () => (dispatch, getState) => {
+ const incrementIfPositive = createSmartThunk(() => (dispatch, getState) => {
const currentValue = getState().value;
if (currentValue <= 0) {
return null;
}
- return dispatch({
- type: INCREMENT_IF_POSITIVE,
- value: currentValue + 1,
- });
+ return { value: currentValue + 1 });
- };
+ });
- const valueReducer = (state = 0, action) => {
- switch (action.type) {
- case RESET_VALUE:
- return 0;
- case SET_VALUE:
- case INCREMENT_IF_POSITIVE:
- return action.value;
- default:
- return state;
- }
- }
+ const valueReducer = createReducer({
+ [resetValue]: () => 0,
+ [joinTypes(setValue, incrementIfPositive)]: (state, action) => action.value;
+ }, 0);
Don’t be afraid that this library is new, I use it in several very big projects already without any issues, so I very recommend you to at least try it! If you happen to like it, any token of appreciation like giving a star to the github repo is very much welcome!
Top comments (2)
Hi,
Do you aware about redux toolkit?
My problem has been solved by that library
Hi,
Yes, but redux-toolkit missed several things important for me, hence I created
redux-smart-actions
. Things I missed:1) I don't like syntax like
const doSomething = createAction('doSomething')
, with my library you don't need to pass this duplicated name at all, saving you a little time and name duplication2)
createSmartAction
from my lib has an automatic ability to protect against type collisions3) toolkit doesn't have something like
createThunk
- thunks creator with attached constants like for normal actions4) I know that 1, 2 is partially solved by slices, but I don't like them, for one thing I prefer writing actions as top level functions, another thing is that slices create reducers, and often I use global reducers and distributed actions, usually with my another library github.com/klis87/redux-requests