The problem only gets worse once you consider sharing state values with sibling components; you might need to include unused parent variables so it can mediate between its two child components needing the same one.
However, setting up Redux can be a chore. Some developers might even find it counterintuitive while others might find the writing of actions and reducers an additional and unwelcome job. Redux Toolkit (RTK), previously known as Redux Starter Kit, provides some options to configure the global store and create both actions and reducers in a more streamlined manner.
How to start using RTK in your project
You will need to include Redux as well as the react-redux binding package for it to work properly with React. You’ll also need the Redux Toolkit itself. The three packages can be added using a nodeJS bundle utility such as npm or yarn, like so:
# NPM
npm i redux react-redux @reduxjs/toolkit
# Yarn
yarn add yarn add redux react-redux @reduxjs/toolkit
Usually, you would also need the Redux DevTools extensions in order to debug your store mutations correctly; but since you’re using RTK, those are already included.
Creating a store
In traditional Redux, you had to call createStore using the main reducer as a parameter. The way to do it with RTK is to call configureStore, which allows you to do the same, but also instantiates RTK to work with other functionalities.
Let’s say we have a basic main reducer function like the one here:
const initialState = {
someProperty: null,
otherProperty: null,
// ...,
finalProperty: null,
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case 'SOME_ACTION':
return {
...state,
someProperty: action.payload,
}
case 'OTHER_ACTION':
return {
...state,
someProperty: action.payload,
}
// ...
case 'FINAL_ACTION':
return {
...state,
finalProperty: action.payload,
}
default:
return state;
}
}
Remember you might also have a reducer combiner function here, but let’s just create a simple one. Now. to create a store with RTK, you must use:
const store = configureStore({
reducer: mainReducer,
})
This creates a store with the mainReducer function at its core using DevTools as well as the default middleware (including thunk and a couple of logging tools). This is all done with configureStore.
Updating the store
Creating actions with RTK is much simpler than with the traditional arrow function which calls a dispatch. To create an action you just have to do something like this:
const someAction = createAction('SOME_ACTION');
And that’s it. The action can be imported elsewhere (like in any of your components) and be called with any payload you choose. Behind the scenes, this will make the usual dispatch call to the reducer, specifying type and using the single parameter when called as a payload. So:
someAction('Hello');
would call the action with the string “Hello” as the payload.
If needed, you might also include some additional logic. Say, for example, you need to invoke an axios call and then send the response as part of the payload. You would then call the createAction helper like this:
const someAction = createAction('SOME_ACTION', async text => {
const response = await axios.get(`/some/api/${text}`);
return {
payload: {
text,
data: response.data,
}:
};
}):
Note that in the return we don’t include either the dispatch call nor the type.
As for the reducers we also have a helper which can streamline the process. The initial reducer we used as an example could be re-written like this:
const initialState = {
someProperty: null,
otherProperty: null,
// ...,
finalProperty: null,
};
const mainReducer = createReducer(initialState, {
[someAction]: (state, action) => {...state, someProperty: action.payload},
[otherAction]: (state, action) => {...state, otherProperty: action.payload},
// ...
[finalAction] : (state, action) => {...state, finalProperty: action.property},
});
Creating all actions and reducers with a single slice
The real differentiator in RTK is the possibility of creating actions and reducers all at once with the use of the new element: slices. In order to create both you would do something like this:
import axios from 'axios';
import {
createSlice,
configureStore,
createAsyncThunk,
} from '@reduxjs/toolkit';
const initialState = {
someProperty: null,
otherProperty: null,
// ...
finalProperty: null,
};
export const someAction = createAsyncThunk(
'test/someAction',
async type => {
const response = await axios.get(`/api/${type}`);
return response.data;
},
);
export const testSlice = createSlice({
name: 'test',
initialState,
reducers: {
otherAction: (state, action) => ({
...state,
otherProperty: action.payload,
}),
// ...
finalAction: (state, action) => ({
...state,
otherProperty: action.payload,
}),
},
extraReducers: {
[someAction.fulfilled]: (state, action) => {
state.someProperty = action.payload;
},
},
});
export const store = configureStore({
reducer: testSlice.reducer,
});
Now, there are several things to notice here. First, we’re importing axios to make a REST get call (although you could use fetch, or any other method you’d prefer). We’re assuming that we have a proxy configured in our packages.json file and an API that receives a type param which will fetch data used in someProperty.
The initial values for the state are configured in a similar manner to the one we had previously used, but starting on the next line things get interesting.
We just used the createAsyncThunk helper for relating our store dispatch to the async logic needed to fetch data from the API. We specified the name of the action as the name we’ll be giving our slice (see next paragraph), followed by a / and the name of the action (in this case, someAction). Then we passed the actual function to the helper, but note there is no mention of the dispatch helper anywhere; it’s just a normal fetching action with a promise returned.
Next comes the slice, which as we stated previously, is a nice and elegant way of defining both actions and reducers at the same time. The slice receives a name (which must be the same name as any names used in async thunks related to this store), an initialState, and then the normal reducers (those that can simply alter the store without the need of any asynchronous operations). These reducers just receive the name of the action which will in turn get the state and action as params and each action will do the needed store reduction.
After the normal reducers come the (optional) extraReducers, which are those that need some extra logic and are related to the store through possible async results. You’ll notice we used the fulfilled suffix after our actual action name. That’s because the actions created by createAsyncThunk can have three possible results: pending, fulfilled and rejected. You might specify some actions in the store while the results are pending, set some results with fulfilled (as we just did), or manage errors in the async function with the rejected suffix.
Finally, we configured our store in a similar way we previously used but the reducers are now provided by our slice.
Putting it all together: referencing reducers and actions from our app
In order to connect the store we defined in your React app, you’ll need to wrap all the content within a <provider></provider> tag. First, import the Provider component.
Using the connect function you can map props in the global state to your local props as well as dispatch actions so you can use them within your component:
import React from 'react';
import { Provider, connect } from 'react-redux';
import { store, testSlice, someAction } from './store';
const { actions } = testSlice;
const { otherAction, finalAction } = actions;
const App = ({
someProperty,
otherProperty,
/* ... */
finalProperty,
someAction,
otherAction,
/* ... */,
finalAction,
}) => (
<Provider store={store}>
<button
onClick={() =>
someAction(1)
}
type="button"
> Test
</button>
</Provider>
);
const mapStateToProps = state => ({
someProperty: state.someProperty,
otherProperty: state.otherProperty,
/* ... */,
finalProperty: state.finalProperty,
});
export default connect(
mapStateToProps,
{ someAction, otherAction, /* ... */, finalAction },
)(App);
The connect helper allows us to map the global state to our props (by using a simple mapStateToProps function) and the actions we want to dispatch. In this manner, we can access all the props from within our component code and dispatch actions to the reducer by simply calling the mapped action.
In conclusion
By using the Redux Toolkit you can write all the code you’ll need for your Redux store in a single file, including actions and reducers, in a much more readable way.