Telerik blogs

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.

What Is Redux?

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;
  }
}

What Problems Redux Solved and Its Pros and Cons

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.

The New Redux with Redux Toolkit (RTK)

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.

Setting up a New React Project

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.

Redux with Redux-Thunk

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:

  • action constants – Values that are used by action creators and reducers for action’s type property.
  • action creators – Functions that create an action object, which then is passed to the dispatch method.
  • thunks/actions – Actions that return an async function, which receives 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.

Posts with pure Redux

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.

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.

Handling API Requests And Server State With Redux Toolkit 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 Postscomponent 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;

Conclusion

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-2
About the Author

Thomas Findlay

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.

Related Posts

Comments

Comments are disabled in preview mode.