Skip to main content
Photo from unsplash: /redux-banner_lwnh1m

Mastering redux and redux toolkit

Written on October 21, 2022 by Kevin

11 min read

Introduction

Let’s start with understanding what Redux is and its purpose of it. Basically, Redux is a library to manage global state in an application. Previously, there’s a lot of problems with how components can share their local state with others. This is where Redux comes to help. In the official document Here, you only need Redux if and only if your application has a medium to large codebase, the global shared state is complex, and frequent updates happening. If it’s not the case, I think React useContext is more than enough. Just remember that there’s no rendering optimization in useContext. You need to be careful structuring your context hook to prevent any unnecessary rendering.

The most difficult part of Redux is the initialization and configuration. Redux team realizes this and decided to create redux-toolkit as a base recommended way to write redux. In addition, it also provides some basic yet useful functionality such as dev tool, async action handler, and others. You might need to check this article to know more

https://redux.js.org/introduction/why-rtk-is-redux-today

Terminology

1. Store

Store is the main part of redux that contains all the global states by combining multiple reducers.

Note that 1 application only needs to have 1 store.

2. Action

Actions are basically a JS object that acts as an event that will be handled by the reducer to change the global state. The usual action structure looks like the following example. Note that the action type must be unique among other actions

{ type: 'ACTION_INCREMENT_NUMBER'; payload: 2; }
js

3. Reducer

Reducer is the main building part of a store where it has direct access to a state and is able to modify it. The reducer takes an action and decides how to modify the state based on the action type.

function counterReducer(state = initialState, action) { // Check to see if the reducer cares about this action if (action.type === 'ACTION_INCREMENT_NUMBER') { return { // update the copy state with the new value ...state, value: state.value + 1, }; } // otherwise return the existing state unchanged return state; }
js

4. Dispatch

Dispatch is just a way to trigger an action. Where we dispatch an action, that action will be handled by the reducer and thus, updating the state.

5. Selector

A component might need a small part of data inside the store. Thus, a selector is used to select the desired and needed data only. Redux is smart enough to determine when the component should be updated based on the selected data only. This is what makes Redux different than useContext

function selectCounterValue = (rootState) => rootState.counter.value
js

How it works?

Resize Rem

The picture above explains the flow of Redux. Redux is a one-way data flow that follows the flux architecture. First, we have the UI that can access the state data inside a store by using a selector. Then, UI can dispatch an action in order to update the global state. This action will be received by the reducer and handled by the reducer logic which will return that updated state. Because the UI is sort of subscribed to the state data, the UI will be rerendered based on the new data.

Notes in developing Redux

  • redux state is immutable, that’s why in every reducer, you need to create a new copy of the updated state
  • never store a non-serializable state data such as set, function, Symbol, Class, and others
  • see more the redux best practice in https://redux.js.org/style-guide/

This flow is perfect, but we miss one important detail. How to handle an asynchronous task/action? In default, everything will be synchronous meaning once the action is dispatched, it will be handled by the reducer directly. One common use case of an async action is when we want to call an API and use the API data to update the global state. Redux handled this by what is called middleware. As the name implies, middleware executes additional logic when action is dispatched. In this case, delaying the dispatched action to wait for the async task to be finished and then sending it to the reducer. There is 2 popular middleware to handle this, Redux thunk and redux saga. They have a different mental model where redux thunk is based on the promises and async await, and redux saga is based on JS generator function where we can pause and continue the function execution. Redux thunk is the most popular because promises are easier to understand.

the example of async action will be like the following:

//A component will dispatch an async action dispatch(fetchUserData()) //an async action, it delays the actual action while waiting for the async task const fetchUserData = () => async (dispatch) => { const data = await callApi() dispatch({ type: 'ACTION_FETCH_USER_DATA' payload: data }) }
js

Redux toolkit comse to help Previously, we are free to write and structure our own version of action, reducer, and store configuration. However, a lot of people do not follow the best practice of writing Redux. Thus, redux toolkit is created to help us write Redux in a recommended way and provides some basic tools where we do not need to add manually. The followings are some Redux toolkit API:

  1. CreateReducer and createAction not recommended

I won’t explain much as this is not a recommended approach. But, you need to be aware of such API exists. Previous code without redux toolkit:

//actions const increment= () => { type: 'increment', payload: null } const incrementByAmount = (amount) => { type: 'incrementByAmount', payload: amount } //reducer const initialState = { value: 0 } function counterReducer(state = initialState, action) { switch (action.type) { case 'increment': return { ...state, value: state.value + 1 } case 'incrementByAmount': return { ...state, value: state.value + action.payload } default: return state } } //to call dispatch(increment())
js
  • with redux toolkit:
//action const increment = createAction('counter/increment'); const incrementByAmount = createAction('counter/incrementByAmount'); //reducer const initialState = { value: 0 }; const counterReducer = createReducer(initialState, (builder) => { builder .addCase(increment, (state, action) => { state.value++; }) .addCase(incrementByAmount, (state, action) => { state.value += action.payload; }); }); //to call dispatch(increment());
js
  • note that, with redux toolkit, we can mutate the state directly. It’s different that the old redux where we need to copy the old state and return the updated one.
  1. ConfigureStore

This is the entry point where it manages all the redux config and combines all reducers into 1 redux store. As shown in the example below, you can put the root reducer that contains some child reducer. The others are optional such as additional middleware and enhancers.

const store = configureStore({ reducer: { counter: counterReducer, user: userReducer }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(.....), devTools: process.env.NODE_ENV !== 'production', preloadedState: {......}, enhancers: [.....], })
js
  1. CreateSlice CreateSlice is the recommended way to create a reducer and action. Basically, it combines the functionality of createAction and createReducer into 1 API. Thus, it guarantees that the action that is generated by the createSlice is unique
const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment(state) { state.value++; }, decrement(state) { state.value--; }, incrementByAmount(state, action) { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; export default counterSlice.reducer;
js
  1. CreateAsyncThunk

Redux toolkit supports redux thunk by default, we can use createAsyncThunk API to create an async action. This action will have 3 different states such as pending, fulfilled, or rejected. You can consider this as 3 different actions generated where we can update the app state based on the action state. In addition, to combine external action into createSlice, we can inject it through extraReducer property as below example:

const fetchUserById = createAsyncThunk( 'USER_FETCH_USER_BY_ID', async (args, { dispatch, getState }) => { const response = await userAPI.fetchById(args.userId) return response.data } ) const usersSlice = createSlice({ name: 'users', initialState: { users: [], isLoading: false }, reducers: { // standard reducer logic, with auto-generated action types per reducer }, extraReducers: (builder) => { // Add reducers for additional action types here, and handle loading state as needed builder.addCase(fetchUserById.fulfilled, (state, action) => { // Add user to the state array state.users.push(action.payload) state.isLoading = false }) .addCase(fetchUserById.pending, (state, action) => { state.isLoading = true }) .addCase(fetchUserById.rejected, (state, action) => { state.isLoading = false //handle error here }), }, }) //to call dispatch(fetchUserById({ userId: 2 }))
js
  • in createAsyncThunk , the first argument is the action type name and the second argument is the handler async function where it takes any value that the user passes as the first argument and the second argument is some Thunk API where you have access to dispatch function in case you want to dispatch another action and getState function to get the root state value.

Example in real app

Let’s dive into an example to make you understand more on how Redux works. To be honest, the redux toolkit already provided the template for us as an example. To get started, try to initialize the app by:

# Redux + Plain JS template npx create-react-app my-app --template redux # Redux + TypeScript template npx create-react-app my-app --template redux-typescript
bash

or to add to existing app

npm install @reduxjs/toolkitnpm install react-redux npm install --save-dev @redux-devtools/core
bash

Let’s create a simple blog application. On the first screen, we will have an input bar where user can input the user id. Once the user click submit button, we want to send an API request to get user data. Once the user data is retrieved, we want to call another API to get the blog the user has created. we will show all the user data and blog data on the screen. Here, All the user state and action will be handled by redux.

let’s start to create our first slice, let’s call it blogSlice.ts

//async action export const fetchBlogsByUserId = createAsyncThunk( 'BLOG_FETCH_BLOGS_BY_USER_ID', async (args) => { const response = await blogAPI.fetchByUserId(args.userId); return response.data; } ); //slice (action + reducer) const blogSlice = createSlice({ name: 'blog', initialState: { blogs: [] }, reducers: {}, extraReducers: (builder) => { builder.addCase(fetchBlogsByUserId.fulfilled, (state, action) => { state.blogs.push(action.payload); }); }, }); //selector export const selectBlogs = (rootState) => rootState.blog.blogs; export default blogSlice.reducer;
js

Next is the user slice, let’s call it userSlice.ts

export const fetchUserById = createAsyncThunk( 'USER_FETCH_USER_BY_ID', async (args, { dispatch }) => { const response = await userAPI.fetchUserById(args.userId); dispatch(fetchBlogsByUserId(args)); //dispatch another action return response.data; } ); const userSlice = createSlice({ name: 'user', initialState: { user: { id: 1, name: '' } }, reducers: {}, extraReducers: (builder) => { builder.addCase(fetchUserById.fulfilled, (state, action) => { state.user = action.payload; }); }, }); export const selectUser = (rootState) => rootState.user.user; export default userSlice.reducer;
js

The most important is the redux store, it just combines all the reducers into

  1. let’s call it Store.ts
import { configureStore } from '@reduxjs/toolkit'; import userReducer from '../userSlice'; import blogReducer from '../blogSlice'; export const store = configureStore({ reducer: { user: userReducer, blog: blogReducer, }, });
js

The next question is how to connect redux to the react component. First, we need to set up a store provider. This is to give a React application the redux store context. If you want to access to the store action and state, all the React applications must be the descendant of the redux Provider , usually, we put it in the root index.ts

import { Provider } from 'react-redux'; import { store } from './App/store'; ... <Provider store={store}> <App /> </Provider> ...
jsx

In the old redux, they still use HOC approach where it injects the redux state and redux action into the component props. Nowadays, hooks are preferred. There are 2 hooks provided by react-redux: useDispatch and useSelector. You will more understand by the below example:

const App = () => { const [userId, setUserId] = useState(''); const dispatch = useDispatch(); const user = useSelector(selectUser); const blogs = useSelector(selectBlogs); const onSubmit = () => { dispatch(fetchUser(userId)); }; return ( <div> <input onChange={(e) => setUserId(e.target.value)} value={userId} /> <button onClick={onSubmit} /> <div>User data: {JSON.stringify(user)}</div> {blogs.map((blog) => ( <div key={blog.id}>{blog.name}</div> ))} </div> ); };
jsx

That’s it. It is almost everything that you need to know about redux. The next important things to know if to create a complex store config and how to structure the redux application to make it more scalable. Maybe I will create another post about it.

As you notice from the example app, almost all the redux state contains data from API. This is actually happening in a lot of real applications. There’s almost none a synchronous task that will update the state directly. That’s why people are switching to a data fetching library. Redux realizes that and finally released RTK-query that is compatible with the current redux toolkit. What is the difference with the OG redux? do we still need Redux?