Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Подробнее о работе слайсов React: Redux Toolkit

Начнем погружаться в Toolkit со слайсов. Сразу с главного. Что бы мы не делали внутри слайсов, в конце концов они генерируют обычные редьюсеры и действия, которые затем передаются в Redux. То есть, слайсы не добавляют никаких новых возможностей в сам Redux. Они автоматизируют рутину, сокращают количество кода и предоставляют более удобные "ручки" для управления действиями и состоянием.

Для создания слайса нам нужно как минимум три компонента: имя, начальное состояние, набор редьюсеров.

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

// Начальное значение
const initialState = {
  value: 0,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // Редьюсеры в слайсах мутируют состояние и ничего не возвращают наружу
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1
    },
    // пример с данными
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
});

Имя

Имя это свойство в состоянии приложения, внутри которого хранятся данные текущего слайса. Кроме того, имя используется как префикс в названии действия. На картинке слева Navigation. Помогает отладке, мы видим откуда взялось действие.

Redux DevTools

Начальное состояние

Под начальным состоянием понимается базовая структура данных и какие-то статические данные, если они есть, например значение 0 для счетчика. А вот те данные, которые нужно выкачать по API, к начальным не относятся. Они заполняются уже потом, через действия.

Редьюсеры

Редьюсеры в Toolkit очень похожи на редьюсеры самого Redux, но имеют несколько важных отличий. Каждый редьюсер соответствует конкретному действию, поэтому внутри нет конструкции switch, а сами редьюсеры очень маленькие. И внутри редьюсеров происходит прямое изменение состояния. Как такое возможно?

Redux, несмотря на свою концептуальную красоту и чистоту, становится неудобным в работе, когда состояние становится глубоко вложенным. Запрет на прямое изменение порождает сложные конструкции, которые приходится писать для обновления глубоко спрятанных данных:

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

Для решения этой проблемы в JavaScript написали немало библиотек, но все они требуют изучения еще одного инструмента, который хоть и сокращает количество кода, но вносит еще один уровень абстракции, со своими проблемами и сложностями использования. Так продолжалось до тех пор, пока не появился Immer. Эта библиотека позволяет отследить прямые изменения внутри объекта, так чтобы обновить оригинал без мутаций, то есть создавая копию в стиле Redux.

import produce from 'immer';

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


// draft содержит такие же данные как и baseState,
// но они обернуты в Proxy для отслеживания изменений
// Эти изменения затем используются для обновления baseState
const nextState = produce(baseState, (draft) => {
  draft[1].done = true;
  draft.push({title: 'Hexlet teach me'});
});

// Разные объекты!
nextState !== baseState;

В отличие от прямого изменения baseState, Immer делает это как редьюсеры в Redux, в неизменяемом стиле.

Получается, что каждый редьюсер в Toolkit, это тот колбек из Immer, в который передается draft. Теперь мы можем мутировать состояние, но внутри все работает так, как будто мы этого не делаем. Благодаря такому подходу сохраняются все возможности, которые предоставляет Redux, включая его DevTool - утилиту для анализа происходящего в браузере. В этом заключается главная фантастика происходящего. Мы получили плюсы от обоих миров, сохранив всю экосистему Redux.

И наконец экспорты. Функция createSlice() генерирует редьюсер и экшены к нему. Всё это официальная документация рекомендует экспортировать в показанном стиле: редьюсер по умолчанию, действия по именам:

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

export default counterSlice.reducer;

Каждый новый редьюсер нужно не забывать добавлять в хранилище:

export default configureStore({
  reducer: {
    counter: counterReducer,
    lessons: lessonsReducer,
    // и все остальные редьюсеры
  },
});

Batch

Когда появляется несколько слайсов, то возникает необходимость обновить состояние сразу несколькими экшенами. Если это делать обычным способом, то на каждое действие будет происходить перерисовка компонента:

// 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 () => {
      // получаем данные users и 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 () => {
  // Вытаскиваем данные из хранилища. 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); // ошибка! users ещё не добавлен в стор
    const body = `Автор: ${author.name}. Текст: ${post.body}.`;
    return <div>{body}</div>;
  };

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

Компонент Posts отрисовывается при каждом изменении состояния. Происходит это дважды: когда мы добавляем посты dispatch(addPosts(posts)) и когда добавляем пользователей dispatch(addUsers(users)). В первом случае возникает проблема, так как пользователи ещё не добавлены, то автор не будет найден. Чтобы этого избежать есть специальная функция batch(), она позволяет объединить несколько обработчиков состояния:

// file: components/App.jsx

import React, { useEffect } from 'react';
// импортируем 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 () => {
      // получаем данные users и posts
      const { data: posts } = await axios.get('/posts');
      const { data: users } = await axios.get('/users');
      // batch принимает функцию, внутри которой мы можем диспатчить экшены
      batch(() => {
        dispatch(addPosts(posts));
        dispatch(addUsers(users));
      });
    };
    fetchData();
  }, []);

  return (<Posts />)
};

Теперь, при загрузке постов и пользователей и добавлении их в стор, компонент Posts отрисуется один раз, когда все данные уже добавлены. Обратите внимание на внутреннюю функцию fetchData(). Согласно документации, функция, передаваемая в useEffect() не должна быть объявлена как асинхронная, поэтому мы создали внутренюю асинхронную функцию.


Дополнительные материалы

  1. Паттерны по обновлению данных в Immer

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
1 июня 10 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»