How To Setup Redux Slices with Redux Toolkit

How To Setup Redux Slices with Redux Toolkit

Redux Toolkit popularity is growing every month. What exactly helps developers to write code faster, easier, more clearly? One of the helpers is createSlice function. createSlice takes an object of reducer functions, a slice name, and an initial state value and lets us auto-generate action types and action creators, based on the names of the reducer functions that we supply. It also helps you organize all of your Redux-related logic for a given slice into a single file.

What is createSlice?

It's a function that deals with everything you need for each slice, do you remember using createAction and createReducer manually? Now it's available in this specific slice function.

So what the returned object from createSlice contains:

  • name: a parameter that will be the prefix for all of your action types
  • initialState: the initial values for our reducer
  • reducers: it's an object where the keys will become action type strings, and the functions are reducers that will be run when that action type is dispatched.

The other benefit of using createSlice is our files structure. We can put all of our Redux-related logic for the slice into a single file. You'll see how to do it in our tutorial.

Slice configuration

We have made basic Redux configuration with Redux Toolkit. But what is the most important benefit of using Toolkit? Definitely, it's createSlice function that you will probably use for most application you're developing.

If you don't want to start from zero, you can use our basic Redux configuration with Redux Toolkit.

To our previous little app with fake authorization, we're gonna add a feature to show users data to logged user.

Firstly let's create file src/store/users.js and create our slice:

import { createSlice } from '@reduxjs/toolkit'

// Slice

const slice = createSlice({
    name: 'users',
    initialState: {
        users: []
    },
    reducers: {
        getUsers: (state, action) => {
            state.users = action.payload;
        },
    },
});

export default slice.reducer

That's basic slice configuration, it contains name, initialState and reducers parameters.

Adding actions

Now let's add actions. To keep it clear and simply add it in our slice's file. We don't have to write them individually in separate file anymore. We can export all the actions. the reducer, the asynchronous thunk and selector that gives us access to state from any component without using connect.

import { createSlice } from '@reduxjs/toolkit'
+ import { api } from '../api/index'

// Slice

const slice = createSlice({
    name: 'users',
    initialState: {
        users: [],
    },
    reducers: {
        usersSuccess: (state, action) => {
            state.users = action.payload;
            state.isLoading = false;
        },
    },
});

export default slice.reducer

// Actions

+ const { usersSuccess } = slice.actions

+ export const fetchUsers = () => async dispatch => {
+    try {
+        await api.get('/users')
+            .then((response) => dispatch(usersSuccess(response.data)))
+    }
+    catch (e) {
+        return console.error(e.message);
+    }
+}

Connecting to store

We have to connect our reducer to the store. Let's import our users reducer and combine them all into one root reducer.

import { configureStore } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'

import user from './user'
+ import users from './users'

const reducer = combineReducers({
  user,
+  users,
})

const store = configureStore({
  reducer,
})

export default store;

Connecting App to API

We prepared basic configuration of our slice but we want to get data to display. We're gonna use JSONPlaceholder - fake online REST API. Let's add Axios to our app.

npm install --save axios

And lets setup our API basic configuration. Create src/api/index.js.

import axios from 'axios'

export const api = axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
    headers: {
        'Content-Type': 'application/json'
    },
})

Data displaying

To extract data from our store we're gonna use useSelector hook. It's equivalent to mapStateToProps argument in connect complex. We can use useSelector() multiple times within a single function component and each call will create individual subscription to the store.

Our first action lets us fetch users data. Let's display data to the user. Create file src/components/usersList.js containing:

import React from "react";
import { useSelector } from "react-redux";
import User from "./user";

const UsersList = () => {
  const { users, isLoading } = useSelector(state => state.users);

  return (
    <table>
      <thead>
        <tr>
          <td>ID</td>
          <td>Name</td>
          <td>Phone</td>
        </tr>
      </thead>
      {!isLoading ? (
        <tbody>
          {users.map(user => (
            <User user={user} />
          ))}
        </tbody>
      ) : (
        <div>Loading...</div>
      )}
    </table>
  );
};

export default UsersList;

We choose users slice from our state and gets users and isLoading data.

Then create src/components/user.js with:

import React from "react";

const User = ({ user: { id, name, phone } }) => {
  return (
    <tr>
      <td>{id}</td>
      <td>{name}</td>
      <td>{phone}</td>
    </tr>
  );
};

export default User;

And update our app.js:

...
function App() {
const dispatch = useDispatch();

if (user) {
  return (
    <div>
      <div>
        Hi, {user.username}!
        <button onClick={() => dispatch(logout())}>Logout</button>
      </div>
+      <UsersList />
    </div>
  );
}

The last step is to fetch the data. We are using useEffect to trigger the action after the component has been mounted.

...
+ import { fetchUsers } from "../store/users";

const UsersList = () => {
+ const dispatch = useDispatch();
const { users, isLoading } = useSelector(state => state.users);

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

return (
...

Errors and loading handling

Next, let's make it complete by adding loading and error handling for better app management.

...
const slice = createSlice({
  name: "users",
  initialState: {
    users: [],
+    isLoading: false,
+    error: false,
  },
  reducers: {
+    startLoading: state => {
+      state.isLoading = true;
+    },
+    hasError: (state, action) => {
+      state.error = action.payload;
+      state.isLoading = false;
+    },
    usersSuccess: (state, action) => {
      state.users = action.payload;
      state.isLoading = false;
    },
  }
});

Update our slice with two new reducers that are gonna control state for loading and errors. We just don't need to create endLoading reducer because as you can see we end loading in each reducer.

After setting up reducers we have to update our actions.

+ const { usersSuccess, startLoading, hasError} = slice.actions;

export const fetchUsers = () => async dispatch => {
+  dispatch(startLoading());
  try {
    await api
      .get("/users")
      .then(response => dispatch(usersSuccess(response.data)));
  } catch (e) {
-   return console.error(e.message);
+   dispatch(hasError(e.message))
  }
};

And here we go, our app is fetching and displaying data now!

Summary

You can still use the old-fashioned way of using Redux but Redux Toolkit simplify things. With createSlice and useSelector we can reduce our boilerplate code and make the code easier to develop and read.

I hope you enjoy our tutorial where we have made our first slice. You can find the complete code repository presented in this article at GitHub.