Зарегистрируйтесь для доступа к 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
    },
  },
});

Имя (name) используется как префикс в названии действия. Оно помогает в отладке — мы видим, откуда взялось действие:

Redux DevTools

Начальное состояние (initialState) — это базовая структура данных и какие-то изначальные данные, если они есть (например, значение 0 для счетчика). Данные, которые нужно выкачать по API, к начальным не относятся. Они заполняются уже потом через действия.

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

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

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

Раньше для решения этой проблемы использовалось множество разных библиотек. Все библиотеки вносили еще один уровень абстракции и делали работу сложнее.

Так продолжалось до тех пор, пока не появилась библиотека 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;

// Новый объект с добавленным элементом
console.log(nextState);
// [
//   { title: 'Learn TypeScript', done: true },
//   { title: 'Try Immer', done: true },
//   { title: 'Hexlet teach me' }
// ]

// Исходный объект не изменился
console.log(baseState);
// [
//   { title: 'Learn TypeScript', done: true },
//   { title: 'Try Immer', done: false }
// ]

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

import produce from 'immer';

// Для примера мы взяли список пользователей с адресами проживания
const baseState = [
  {
    login: 'user1',
    contact: {
      phoneNumber: '111-1111111',
      emailAddress: 'user1@example.com',
    },
    address: {
      streetAddress: '123',
      city: 'Some City',
      postalCode: '1111111',
    },
  },
  {
    login: 'user2',
    contact: {
      phoneNumber: '222-222222',
      emailAddress: 'user2@example.com',
    },
    address: {
      streetAddress: 'street 1',
      city: 'Old City',
      postalCode: '123456',
    },
  },
];

// Для примера представим, что один из пользователей переехал — нужно обновить адрес
// Меняем адрес, не меняя исходный объект
const nextState = produce(baseState, (draft) => {
  draft[1].address.city = 'New City';
  draft[1].address.postalCode = '33333333';
  draft[1].address.streetAddress = 'new street 2';
});

// Новое состояние с обновленным адресом
console.log(nextState);
// [
//   {
//     login: 'user1',
//     contact: { phoneNumber: '111-1111111', emailAddress: 'user1@example.com' },
//     address: { streetAddress: '123', city: 'Some City', postalCode: '1111111' }
//   },
//   {
//     login: 'user2',
//     contact: { phoneNumber: '222-222222', emailAddress: 'user2@example.com' },
//     address: {
//       streetAddress: 'new street 2',
//       city: 'New City',
//       postalCode: '33333333'
//     }
//   }
// ]

// Исходное состояние не изменилось
console.log(baseState);
// [
//   {
//     login: 'user1',
//     contact: { phoneNumber: '111-1111111', emailAddress: 'user1@example.com' },
//     address: { streetAddress: '123', city: 'Some City', postalCode: '1111111' }
//   },
//   {
//     login: 'user2',
//     contact: { phoneNumber: '222-222222', emailAddress: 'user2@example.com' },
//     address: {
//       streetAddress: 'street 1',
//       city: 'Old City',
//       postalCode: '123456'
//     }
//   }
// ]

https://replit.com/@hexlet/js-redux-toolkit-immer#index.js

Каждый редьюсер в 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,
    // И все остальные редьюсеры
  },
});

Передаваемый в редьюсер объект формирует глобальное состояние Redux. В примере выше состояние будет выглядеть так:

{ counter, lessons }

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

  1. Паттерны обновления данных в Immer
  2. Как проверить state внутри reduce?

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

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

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 28 ноября
профессия
от 39 525 ₸ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 28 ноября

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

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

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»