Learn how to use Redux in a modern way by taking advantage of Redux Toolkit and Redux Toolkit Query to reduce boilerplate code for managing shared state and server data.
For many years, Redux wore the crown of the most popular state management choice for React applications. It provided a predictable approach to handling global and shared state management in modern applications that were becoming more dynamic and required better ways to handle state updates.
Despite all of its shortcomings and the rise of new state management solutions, such as Zustand or Jotai, React-Redux, which provides official React bindings for Redux, is still the most downloaded library for state management in React apps.
However, it is not because of Redux itself, as large applications usually become very complex due to all the boilerplate that Redux requires. Instead, what keeps Redux still alive is the Redux Toolkit library.
In this article, we will cover how to use Redux in a modern way by taking advantage of Redux Toolkit and Redux Toolkit Query.
The core concepts of Redux are are state, actions and reducers. Redux maintains the entire state in a single object, which makes it easier to track and manage data flow and acts as the source of truth for the state in our app.
To update the state, we need to dispatch an action, which is a plain JavaScript object that must contain a type
property. It is used to indicate what kind of action is being performed.
The snippet below is an example of an action that should increment state by the specified amount
.
{
type: 'INCREMENT',
amount: 1
}
Finally, reducers decide how the Redux state should change based on the action that was dispatched. Reducers are pure functions that use the current state and the action dispatched to return a new state.
Here’s an example of a simple counter
reducer:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + action.amount;
case 'DECREMENT':
return state - action.amount;
default:
return state;
}
}
Redux very quickly became the most popular state management solution for React apps. Applications became easier to maintain due to the state centralization, relying on actions to update it and the fact that Redux enforced state immutability, which resulted in more predictable behavior and easier debugging.
What’s more, Redux DevTools made state debugging much easier, as they offered powerful features, such as time-travel debugging, which allowed developers to step through the state changes. With a large community and ecosystem, it was also easy to enhance Redux’s functionality by extending it with middleware plugins. For example, persisting Redux’s state is as simple as adding the Redux Persist middleware.
However, working with Redux was not all sunshine and roses. One of the most common complaints about Redux is that it requires a large amount of boilerplate code, even for a simple state.
To rub more salt into the wound, Redux doesn’t even allow asynchronous operations and requires additional libraries for it, such as Redux Thunk or Redux Saga. This caused defragmentation of the ecosystem and resulted in React apps having different ways of performing async operations, even though all were using Redux.
Furthermore, a lot of developers misused Redux and instead of using it only for truly global or shared state, they used it for all kinds of states. Therefore, many React applications were much more complex and bloated with unnecessary code than they should have been. Fortunately, a new way of using Redux is now available—Redux Toolkit.
Redux Toolkit (RTK) is the official, opinionated, batteries-included toolset for efficient Redux development. It’s a wrapper around the core Redux functionality combined with Redux Thunk and Immer providing utilities to simplify actions, reducers and store creation.
When it comes to code, the best way to learn something is by practice, so let’s create a new React project to showcase the difference between pure Redux and Redux Toolkit. We will use Vite to create a React project.
npm create vite@latest the-guide-to-modern-redux -- --template react
Next, navigate into the directory and install the required libraries for working with Redux and Redux Toolkit.
cd the-guide-to-modern-redux
npm install redux redux-thunk react-redux @reduxjs/toolkit
npm run dev
The dev
command will start the Vite server on http://localhost:5173
You can find the full code example for this article in this GitHub repository. Below, you can also find an interactive Stackblitz example.
Let’s start by creating functionality to fetch and display a list of posts using pure Redux. This will showcase well how much boilerplate code is needed to handle fetching and storing some data.
First, let’s create a Redux store, actions and reducers to handle fetching and storing posts in a Redux store, as well as modifying a loading state to indicate an API request is performed.
src/components/redux/store/redux.store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import { postsReducer } from "./posts.reducer";
import thunk from "redux-thunk";
const appReducers = combineReducers({
posts: postsReducer,
});
export const store = createStore(appReducers, applyMiddleware(thunk));
The store initialization is quite simple. The combineReducers
method is used to combine multiple reducers, which are then passed to the createStore
method. For this example, we don’t really need combineReducers
, but it is commonly used, as rarely applications have just one reducer. Further, we apply the thunk
middleware, as Redux by itself doesn’t support asynchronous operations.
src/components/redux/store/posts.actions.js
export const POSTS_ACTIONS = {
SET_IS_LOADING_FETCH_POSTS: "SET_IS_LOADING_FETCH_POSTS",
FETCH_POSTS: "FETCH_POSTS",
SET_POSTS: "SET_POSTS",
};
export const setIsLoadingFetchPosts = isLoading => {
return {
type: POSTS_ACTIONS.SET_IS_LOADING_FETCH_POSTS,
payload: isLoading,
};
};
export const setPostsAction = posts => {
return {
type: POSTS_ACTIONS.SET_POSTS,
payload: posts,
};
};
export const fetchPostsAction = () => {
return async dispatch => {
try {
dispatch(setIsLoadingFetchPosts(true));
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
);
const data = await response.json();
dispatch(setPostsAction(data.slice(0, 10)));
} catch (error) {
} finally {
dispatch(setIsLoadingFetchPosts(false));
}
};
};
In the posts.actions.js
file, we have:
type
property.dispatch
method.dispatch
and getState
methods as arguments. This functionality is provided by the Redux-Thunk
library. In the code above, we have fetchPostsAction
, which fetches a list of posts and updates the posts
and isLoadingPosts
states accordingly.Next, let’s create the posts reducer.
src/components/redux/store/posts.reducer.js
import { POSTS_ACTIONS } from "./posts.actions";
const initialState = {
isLoadingPosts: false,
posts: [],
};
export const postsReducer = (state = initialState, action) => {
switch (action.type) {
case POSTS_ACTIONS.SET_POSTS:
return {
...state,
posts: action.payload,
};
case POSTS_ACTIONS.SET_IS_LOADING_FETCH_POSTS:
return {
...state,
isLoadingPosts: action.payload,
};
default:
return state;
}
};
The postsReducer
is responsible for storing the posts
array and isLoadingPosts
flag. When the reducer is called, it will return a new state based on the dispatched action. In the switch statement, the action.type
is checked to determine whether the reducer should return a new state or the previous one.
Now, we need to create a component that will dispatch the action to fetch a list of posts and display them.
src/components/redux/Posts.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchPostsAction } from "./store/posts.actions";
const Posts = () => {
const { posts, isLoadingPosts } = useSelector(state => state.posts);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPostsAction());
}, []);
return (
<div>
<h1>Pure Redux Example</h1>
{isLoadingPosts ? (
<div>Loading...</div>
) : (
<div>
<ul
style={{
listStyle: "none",
}}
>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
)}
</div>
);
};
export default Posts;
Last but not least, we need to render the Posts
component and wrap it with the Provider
from react-redux
.
src/App.jsx
import { Provider } from "react-redux";
import "./App.css";
import Posts from "./components/redux/Posts";
import { store as pureReduxStore } from "./components/redux/store/redux.store";
function App() {
return (
<>
<Provider store={pureReduxStore}>
<Posts />
</Provider>
</>
);
}
export default App;
The image below shows what the posts should look like.
In order to fetch and display a list of posts, we had to create multiple files and a lot of code, and this example doesn’t even have error handling. Let’s implement the same functionality using Redux Toolkit.
Similarly to the pure Redux example, we need to configure a Redux store. However, this time, we do it using the configureStore
method from the @reduxjs/toolkit
package.
src/components/redux-toolkit/store/rtk.store.js
import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "./posts.slice";
export const store = configureStore({
reducer: {
posts: postsReducer,
},
});
When working with Redux Toolkit, we don’t need to create action creators and action types ourselves. Instead, we can use the createSlice
method, which accepts the following: the initial state, the object of reducer functions and the name for the slice. What’s more, we can combine it with the createAsyncThunk
method when we need to perform any async operations.
src/components/redux-toolkit/store/posts.slice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchPosts = createAsyncThunk("posts/fetch", async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return response.json();
});
const initialState = {
posts: [],
isLoadingPosts: false,
};
const postsSlice = createSlice({
name: "posts",
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(fetchPosts.pending, (state, action) => {
return {
...state,
isLoadingPosts: true,
};
});
builder.addCase(fetchPosts.fulfilled, (state, action) => {
return {
...state,
posts: action.payload.slice(0, 10),
isLoadingPosts: false,
};
});
},
});
export default postsSlice.reducer;
Since the posts
data comes from an API, we don’t define any reducers. Instead, we add extra reducers, which are based on the state of the fetchPosts
thunk. When the fetchPosts
thunk is in progress, the isLoadingPosts
state is set to true
, and when it’s fulfilled, values of isLoadingPosts
and posts
are updated accordingly.
Finally, let’s create a new Posts
component.
src/components/redux-toolkit/Posts.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchPosts } from "./store/posts.slice";
const Posts = () => {
const { posts, isLoadingPosts } = useSelector(state => state.posts);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPosts());
}, []);
return (
<div>
<h1>Redux Toolkit Example</h1>
{isLoadingPosts ? (
<div>Loading...</div>
) : (
<div>
<ul
style={{
listStyle: "none",
}}
>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
)}
</div>
);
};
export default Posts;
The Posts
component is almost exactly the same as the one in the pure Redux example. We just had to change the action method called in the useEffect
. Instead of fetchPostsAction
, the fetchPosts
method from the posts.slice.js
file is used.
The last thing to do is to update the App
component.
src/App.jsx
import { Provider } from "react-redux";
import "./App.css";
import Posts from "./components/redux/Posts";
import RtkPosts from "./components/redux-toolkit/Posts";
import { store as pureReduxStore } from "./components/redux/store/redux.store";
import { store as rtkStore } from "./components/redux-toolkit/store/rtk.store";
function App() {
return (
<>
<Provider store={pureReduxStore}>
<Posts />
</Provider>
<Provider store={rtkStore}>
<RtkPosts />
</Provider>
</>
);
}
export default App;
With Redux Toolkit, we had to write much less code than with pure Redux to achieve the same functionality. However, that’s not all, as Redux Toolkit also offers an addon called Redux Toolkit Query (RTK Query), which can be used to deal with API requests and to handle server state. Let’s implement the posts functionality using RTK Query.
Redux Toolkit Query is a powerful tool for data fetching and caching, which simplifies working with API requests. First, instead of using createSlice
and createAsyncThunk
methods, we will utilize createApi
and fetchBaseQuery
.
src/components/redux-toolkit-query/store/posts.api-slice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const postsApiSlice = createApi({
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com",
}),
endpoints: builder => ({
getPosts: builder.query({
query: () => "posts",
}),
}),
});
export const { useGetPostsQuery } = postsApiSlice;
Now we have much less code than in the previous example. We provide the results of the fetchBaseQuery
method to the baseQuery
option. fetchBaseQuery
is a wrapper on top of the native fetch
. Then, we configure the endpoints. In this case, we configure only one endpoint to fetch posts.
Besides exporting the postsApiSlice
, we also export the useGetPostsQuery
, which is an auto generated hook. Redux Toolkit automatically generates hooks for all endpoints.
Next, we need to configure a new store. The setup is a bit different than with just Redux Toolkit. We need to configure the reducer that is generated by the API slice, add a middleware and setup listeners.
src/components/redux-toolkit-query/store/rtk-query.store.js
import { configureStore } from "@reduxjs/toolkit";
import { postsApiSlice } from "./posts.api-slice";
import { setupListeners } from "@reduxjs/toolkit/query";
export const store = configureStore({
reducer: {
[postsApiSlice.reducerPath]: postsApiSlice.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(postsApiSlice.middleware),
});
setupListeners(store.dispatch);
Next, let’s create a new Posts
component that will utilize the useGetPostsQuery
hook.
src/components/redux-toolkit-query/Posts.jsx
import { useGetPostsQuery } from "./store/posts.api-slice";
const Posts = () => {
const { isLoading: isLoadingPosts, data: posts = [] } = useGetPostsQuery();
return (
<div>
<h1>Redux Toolkit Query Example</h1>
{isLoadingPosts ? (
<div>Loading...</div>
) : (
<div>
<ul
style={{
listStyle: "none",
}}
>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
)}
</div>
);
};
export default Posts;
With Redux Toolkit Query, we were able to remove some code, as we don’t have to rely on useEffect
, useSelector
and useDispatch
. The API slice hooks handle all of this, and we don’t need to dispatch actions or handle loading states manually.
Last but not least, let’s render the new Posts
component in the App.jsx
file.
src/App.jsx
import { Provider } from "react-redux";
import "./App.css";
import Posts from "./components/redux/Posts";
import RtkPosts from "./components/redux-toolkit/Posts";
import RtkQueryPosts from "./components/redux-toolkit-query/Posts";
import { store as pureReduxStore } from "./components/redux/store/redux.store";
import { store as rtkStore } from "./components/redux-toolkit/store/rtk.store";
import { store as rtkQueryStore } from "./components/redux-toolkit-query/store/rtk.store";
function App() {
return (
<>
<Provider store={pureReduxStore}>
<Posts />
</Provider>
<Provider store={rtkStore}>
<RtkPosts />
</Provider>
<Provider store={rtkQueryStore}>
<RtkQueryPosts />
</Provider>
</>
);
}
export default App;
Integrating Redux Toolkit with Redux Toolkit Query in a project can significantly reduce the amount of required boilerplate code to manage shared state and server data.
RTK Query not only simplifies handling API requests but also provides automated caching, invalidation, garbage collection and more. Therefore, if you’re starting a new project and want to use Redux, make sure to use Redux Toolkit to save yourself from writing a lot of unnecessary code.
We only covered a small subset of features offered by Redux Toolkit, so ake sure to check out its docs.
Thomas Findlay is a 5-star rated mentor, full-stack developer, consultant, technical writer and the author of “React - The Road To Enterprise” and “Vue - The Road To Enterprise.” He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP and more. Thomas has worked with developers and teams from beginner to advanced and helped them build and scale their applications and products. Check out his Codementor page, and you can also find him on Twitter.