Register to get access to free programming courses with interactive exercises

Slices React: Redux Toolkit

Let's start diving into Toolkit by looking at slices from the main point. Whatever we do inside slices, they generate the usual reducers and actions, which we pass to Redux.

In other words, slices do not add any new features to Redux. They automate routines, reduce the amount of code, and provide more convenient handles for controlling actions and states.

Let us create a slice. We need at least three components — a name, an initial state, and a set of reducers:

import { createSlice } from '@reduxjs/toolkit';

// Initial value
const initialState = {
  value: 0,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // Reducers in slices mutate the state and return nothing to the outside
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1
    },
    // An example with some data
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
});

Name

We use the name as a prefix when naming the action. In the picture to the left, you see the Navigation. It helps to debug by showing us where the action came from:

Redux DevTools

Initial state

The initial state refers to the underlying data structure and some initial data, if any. It can be the value 0 for a counter, for example. Note that the initial state does not include data you pump out via the API. We fill them in later through actions.

Reducers

Reducers in Toolkit are similar to those in Redux but with some differences. Each reducer corresponds to a specific action, so there's no switch construction inside, and the reducers themselves are very small. And there is a direct change of state within the Reducers. How is that possible?

Despite the conceptual beauty and purity of Redux, it becomes inconvenient to work with when a state is deeply nested. The prohibition of direct changes generates complex constructions. We have to write them to update deeply hidden data:

{
  ...state,
  firstLevel: {
    ...state.firstLevel,
    secondLevel: {
      ...state.firstLevel.secondLevel,
      thirdLevel: {
        ...state.firstLevel.secondLevel.thirdLevel,
        property1: action.data
      },
    },
  },
}

There are many libraries in JavaScript to solve this problem, but they all require learning another tool. This tool reduces the amount of code but introduces another level of abstraction with all its challenges and complexities of use. It went on until the Immer appeared. This library allows you to trace direct changes within an object to update the original without mutations. In other words, we create a Redux-style copy:

import produce from 'immer';

const baseState = [
  {
    title: "Learn TypeScript",
    done: true
  },
  {
    title: "Try Immer",
    done: false
  },
];


// The draft contains the same data as `baseState`, but it is wrapped in `Proxy`
// So we can track changes and use the info to update `baseState`
const nextState = produce(baseState, (draft) => {
  draft[1].done = true;
  draft.push({title: 'Hexlet teach me'});
});

// They are different objects
nextState !== baseState;

Immer handles it like the reducers in Redux in an unchangeable style instead of directly changing the baseState.

Every reducer in Toolkit works like a callback from Immer, into which we pass the draft. Now we can mutate the state, but internally, everything works as if we didn't. This approach preserves all features of Redux, including its DevTool, the utility for analyzing what happens in the browser. And this is the best part. We get the perks of both worlds, keeping the entire Redux ecosystem intact.

And finally, exports. The createSlice() function generates a re-renderer and its actions. The official documentation recommends exporting reducers by default and actions by names:

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;

Don't forget to add each new directory to the store:

export default configureStore({
  reducer: {
    counter: counterReducer,
    lessons: lessonsReducer,
    // And all other reducers
  },
});

Batch

In cases with several slices, you should update the state with several simultaneous actions. If you do it one by one, a component will be redrawn:

// file: components/App.jsx

import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { addUsers } from '../slices/usersSlice.js';
import { addPosts } from '../slices/postsSlice.js';
import Posts from './Posts.jsx';

export default () => {
  const dispatch = useDispatch();

  useEffect(() => {
    const fetchData = async () => {
      // Get data from users and posts
      const { data: posts } = await axios.get('/posts');
      const { data: users } = await axios.get('/users');
      dispatch(addPosts(posts));
      dispatch(addUsers(users));
    });
    fetchData();
  });

  return (<Posts />)
};
// file: components/Posts.jsx

export default () => {
  // Pulling data from the store. state means the whole state
  const users = useSelector((state) => state.usersSlice.users);
  const posts = useSelector((state) => state.postsSlice.posts);

  const renderPost = (post) => {
    const author = users.find((user) => user.id === post.authorId);
    // Error! Users not yet added to the store
    const body = `Author: ${author.name}. Text: ${post.body}.`;
    return <div>{body}</div>;
  };

  return (
    {posts.map(renderPost)}
  );
};

The code renders the Posts component whenever the state changes. It happens twice: when we add posts dispatch(addPosts(posts)) and when we add users dispatch(addUsers(users)).

In the first case, we have a problem. We cannot find the author since we have not added the users yet. To avoid this, we have a batch() function, which allows you to combine several state handlers:

// file: components/App.jsx

import React, { useEffect } from 'react';
// importing `batch`
import { batch } from 'redux';
import { useDispatch } from 'react-redux';
import { addUsers } from '../slices/usersSlice.js';
import { addPosts } from '../slices/postsSlice.js';
import Posts from './Posts.jsx';

export default () => {
  const dispatch = useDispatch();

  useEffect(() => {
    const fetchData = async () => {
      // getting the data of users and posts
      const { data: posts } = await axios.get('/posts');
      const { data: users } = await axios.get('/users');
      // `batch` takes a function that we can use to dispatch actions
      batch(() => {
        dispatch(addPosts(posts));
        dispatch(addUsers(users));
      });
    };
    fetchData();
  }, []);

  return (<Posts />)
};

In this case, when uploading posts and users and adding them to the table, the Posts component is rendered once — after we have added all the data. Note the internal function fetchData(). According to the documentation, we shouldn't declare the function passed to useEffect() as asynchronous, so we created an internal asynchronous function.


Recommended materials

  1. Patterns for updating data in Immer

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.