React Redux Toolkit Crash Course (createAsyncThunk, createSlice)
React Redux Toolkit Crash Course (createAsyncThunk, createSlice)

In this video you will learn everything that you need to know about Redux toolkit.

Not so long ago I made a full video about using Redux from start to the end and a lot of people asked why I didn't mention Redux toolkit and if it makes sense to use it.

So what is Redux toolkit? It's a sugar around plain Redux. It's just a bunch of functions which allow us to write small amount of code. It's not a new way to write Redux applications, you still have the same plain Redux underneath so it's extremely important to fully understand and use basic Redux concepts before you will start using Redux toolkit.

So here I have a React Redux application with Redux hooks, reducers and selectors. Now we will convert it to Redux toolkit so you can see the difference.

First of all we need to install new package.

npm install @reduxjs/toolkit

As you can see we have quite a lot of code to create store properly, add inside redux devtools and middlewares like redux-thunk. From toolkit we get a single command which just accepts a root reducer. What is important we get redux-devtools and redux-thunk as 2 most popular packages out of the box.

const store = configureStore({ reducer });

So we provide inside an object with property reducer. It's also important that we should not combine our root reducers anymore, We can simply throw inside an object with reducers as values.

Also inside there is an additional middleware to check that we won't accidentally mutate state in reducers which is quite beneficial for beginners.


Next thing is to clean up our packages. We can remove redux redux-thunk and reselect. All these 3 libraries are dependencies of redux toolkit. We still use them underneath but we don't need to install them directly.

This is why we need to change the import of createSelector

import { createSelector } from "@reduxjs/toolkit";

So it is still the same createSelector from reselect library but now it all comes from toolkit.


Now let's talk about writing reducers. Previously we wrote just a function with switches inside which from my perspective not bad at all and cover all our needs.

The first way to write reducers is by using createReducer. There are different ways to do that.

export const addUser = createAction("addUser");

export default createReducer(initialState, (builder) => {
  builder.addCase(addUser, (state) => {
    state.users.push(state.username);
    state.username = "";
  });
});

So here we have a createReducer which we use instead of switch. We provide inside initialState and a function which build as a first argument. It allows us to add cases to reducer like with switch. But there are 2 major differences. First of all we don't use string to match action type. We must provide here an action creator. Just to remind you action create is a function which returns action when it is called. In toolkit we have a createAction helper to create an action creator for us. So now all our cases are based on action creators and not on plain strings.

The next important difference which makes everything complicated is immerjs. As you can see I wrote plain push and assign which were completely forbidden in reducers before. But Redux toolkit uses immerjs library underneath which still makes our code immutable but we can write it in a mutable way.

From my point of view it's completely wrong. Now we have 1 more point of failure because there are tricky cases how immerjs will work without your code so you need to learn it additionally and it doesn't force you to think and write your code in functional way which leads to bad code.

So it was first variant how we write reducers with createAction and createReducer. But we also have other variant which is much more popular. It's writing reducers by creating slices.

const usersSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    addUser(state) {
      state.users.push(state.username);
      state.username = "";
    },
    changeUsername(state, action) {
      state.username = action.payload;
    },
    changeSearch(state, action) {
      state.search = action.payload;
    },
  },
});

export const { addUser, changeSearch, changeUsername } = usersSlice.actions;
export default usersSlice.reducer;

So here our code looks similar. We provide a name, initialState and reducers as a object. Also we still have here immerjs inside and we can write code in mutable way. But as you can see there are no action creators here. But actually they are still there. Now our keys of reducers object are action creators and we can export them from our slice to use in all our components.

So the main benefit of slice from createReducer function is that we get our action creators just out of the box. So we write less code.


Now we also need to use our actionCreators inside App

import {
  addUser as addUserAction,
  changeSearch as changeSearchAction,
  changeUsername as changeUsernameAction,
} from "./store/reducers/users";

const handleUser = (e) => {
  dispatch(changeUsernameAction(e.target.value));
};

const handleSearch = (e) => {
  dispatch(changeSearchAction(e.target.value));
};

const addUser = () => {
  dispatch(addUserAction());
};

One more important point is how we can write async actions with createAsyncThunk from toolkit. It's a sugar around redux thunk. The main idea is that we provide the base name of actions that we want to generate and a function that returns a promise. In our case just for testing we can create an async function which will generate for us promise which returns the array of users.

Inside createSlice we can create extraReducers property where we can add reducer cases in builder way like in createReducer. createAsyncThunk gives us pending, fulfilled and rejected action creators to react to.

export const fetchUsers = createAsyncThunk("users/fetchUsers", async () => {
  return ["Jack", "John"];
});

const usersSlice = createSlice({
  ...
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.users = action.payload;
      });
  },
});

Now we can use the actionCreator that createAsyncThunk generated for us directly in components.

useEffect(() => {
  dispatch(fetchUsers());
}, []);

As you can see now on initialze of our component we can dispatch our async action creator.

So this are most important concepts of Redux toolkit. I'm actually not a big fan of it because it adds complexity, additional wrappers which makes debugging more difficult and hides the necessity of immutable data in reducers.

Also if you want to improve your programming skill I have lots of full courses regarding different web technologies.

📚 Source code of what we've done