Перейти к основному содержимому

Redux Essentials, Часть 3: Базовый поток данных в Redux

Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Что вы узнаете
  • Как настроить Redux-хранилище в React-приложении
  • Как добавлять "срезы" (slices) логики редюсеров в хранилище с помощью createSlice
  • Чтение данных Redux в компонентах с помощью хука useSelector
  • Отправка действий в компонентах с помощью хука useDispatch
Предварительные требования

Введение

В Части 1: Обзор и концепции Redux мы рассмотрели, как Redux помогает создавать поддерживаемые приложения, предоставляя единое централизованное место для хранения глобального состояния приложения. Мы также обсудили основные концепции Redux: отправку объектов действий, использование функций-редюсеров, возвращающих новые значения состояния, и написание асинхронной логики с помощью санков (thunks). В Части 2: Структура приложения с Redux Toolkit мы увидели, как API вроде configureStore и createSlice из Redux Toolkit вместе с Provider и useSelector из React-Redux позволяют писать логику Redux и взаимодействовать с ней из React-компонентов.

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

Мы будем использовать синтаксис TypeScript для написания кода. Вы можете использовать Redux с чистым JavaScript, но TypeScript помогает предотвратить распространённые ошибки, предоставляет встроенную документацию для вашего кода и позволяет редактору показывать необходимые типы переменных в таких местах, как React-компоненты и редюсеры Redux. Мы настоятельно рекомендуем использовать TypeScript для всех Redux-приложений.

Внимание!

Пример приложения не является законченным продакшен-решением. Его цель — помочь изучить API Redux и типовые паттерны использования, а также указать верное направление на ограниченных примерах. Кроме того, некоторые ранние реализации позже будут обновлены для демонстрации более оптимальных подходов. Прочитайте всё руководство целиком, чтобы увидеть все концепции в действии.

Настройка проекта

Для этого руководства мы подготовили предварительно настроенный стартовый проект, в котором уже установлены React и Redux, добавлена базовая стилизация и реализован фейковый REST API для написания реальных API-запросов в приложении. Вы будете использовать его как основу для написания кода приложения.

Для начала откройте и форкните этот CodeSandbox:

Вы также можете клонировать тот же проект из этого репозитория Github. Проект настроен на использование Yarn 4 в качестве менеджера пакетов, но вы можете использовать любой менеджер (NPM, PNPM или Bun). После установки пакетов запустите локальный сервер разработки командой yarn dev.

Если вы хотите увидеть финальную версию того, что мы будем создавать, посмотрите ветку tutorial-steps-ts или готовый пример в этом CodeSandbox.

Мы благодарим Танию Рашию, чей туториал Использование Redux с React вдохновил пример на этой странице. Также здесь используется её стартовый CSS-фреймворк Primitive UI для стилей.

Создание нового проекта на Redux + React

После завершения этого туториала вы, вероятно, захотите попробовать создать собственный проект. Мы рекомендуем использовать шаблоны Redux для Vite и Next.js — это самый быстрый способ создать новый проект на Redux + React. Шаблоны уже включают настроенные Redux Toolkit и React-Redux, используя пример счётчика из Части 1. Это позволяет сразу приступить к написанию кода приложения без необходимости добавлять пакеты Redux и настраивать хранилище.

Изучение стартового проекта

Давайте кратко рассмотрим содержимое стартового проекта:

  • /public: базовые CSS-стили и статические файлы, например, иконки

  • /src

    • main.tsx: входной файл приложения, который рендерит компонент <App>. В этом примере он также настраивает фейковое REST API при загрузке страницы.
    • App.tsx: главный компонент приложения. Рендерит верхнюю навигационную панель и обрабатывает клиентскую маршрутизацию для остального контента.
    • index.css: стили для всего приложения
    • /api
      • client.ts: небольшой клиент-обёртка для fetch, позволяющий выполнять HTTP-запросы GET и POST
      • server.ts: предоставляет фейковое REST API для наших данных. Позже наше приложение будет получать данные из этих эндпоинтов.
    • /app
      • Navbar.tsx: рендерит верхний заголовок и навигацию

Если вы запустите приложение сейчас, то увидите заголовок и приветственное сообщение, но без какой-либо функциональности.

Итак, давайте начнём!

Настройка хранилища Redux

Сейчас проект пуст, поэтому нам нужно начать с одноразовой настройки частей, связанных с Redux.

Добавление пакетов Redux

Если посмотреть на package.json, можно увидеть, что необходимые для работы с Redux пакеты уже установлены:

  • @reduxjs/toolkit: современный пакет Redux, включающий все функции, которые мы будем использовать для создания приложения

  • react-redux: функции, необходимые для взаимодействия React-компонентов с хранилищем Redux

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

Создание хранилища

Первый шаг — создать само хранилище Redux. Один из принципов Redux — в приложении должно быть только одно хранилище.

Обычно мы создаём и экспортируем экземпляр хранилища Redux в отдельном файле. Конкретная структура папок зависит от ваших предпочтений, но общепринято размещать общесистемные настройки в папке src/app/.

Мы начнём с создания файла src/app/store.ts и настройки хранилища.

Redux Toolkit включает метод configureStore. Эта функция создаёт новый экземпляр хранилища Redux. У неё есть несколько опций, которые можно передать для изменения поведения хранилища. Она также автоматически применяет наиболее распространённые и полезные настройки, включая проверку типичных ошибок и подключение расширения Redux DevTools для просмотра состояния и истории действий.

src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import type { Action } from '@reduxjs/toolkit'

interface CounterState {
value: number
}

// An example slice reducer function that shows how a Redux reducer works inside.
// We'll replace this soon with real app logic.
function counterReducer(state: CounterState = { value: 0 }, action: Action) {
switch (action.type) {
// Handle actions here
default: {
return state
}
}
}

export const store = configureStore({
// Pass in the root reducer setup as the `reducer` argument
reducer: {
// Declare that `state.counter` will be updated by the `counterReducer` function
counter: counterReducer
}
})

configureStore всегда требует опцию reducer. Обычно это объект, содержащий отдельные "слайс-редьюсеры" для разных частей приложения. (При необходимости можно создать корневой редьюсер отдельно и передать его как аргумент reducer.)

На этом этапе мы передаём фиктивный редьюсер для слайса counter, чтобы продемонстрировать настройку. Вскоре мы заменим его настоящим редьюсером для нашего приложения.

Настройка с Next.js

Если вы используете Next.js, процесс настройки требует дополнительных шагов. Подробности см. на странице Настройка с Next.js.

Предоставление хранилища

Redux сам по себе — это библиотека на чистом JavaScript, которая может работать с любым UI-слоем. В нашем приложении мы используем React, поэтому нам нужен способ взаимодействия React-компонентов с хранилищем Redux.

Для этого мы используем библиотеку React-Redux и передаём Redux-хранилище в компонент <Provider>. Это задействует Context API React, делая хранилище доступным для всех React-компонентов приложения.

Совет

Не следует напрямую импортировать хранилище Redux в другие файлы приложения! Поскольку у нас только один файл хранилища, прямой импорт может вызвать циклические зависимости (когда файл A импортирует B, который импортирует C, который импортирует A), приводящие к сложным ошибкам. Кроме того, нам нужно тестировать компоненты и Redux-логику, а тестам потребуется создавать собственные экземпляры хранилища. Передача хранилища через Context решает эти проблемы.

Для реализации импортируем store в точку входа main.tsx и обернём компонент <App> в <Provider> с хранилищем:

src/main.tsx
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './app/store'

// skip mock API setup

const root = createRoot(document.getElementById('root')!)

root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)

Инспекция состояния Redux

Теперь, когда у нас есть хранилище, мы можем использовать расширение Redux DevTools для просмотра текущего состояния.

Откройте инструменты разработчика в браузере (например, через "Инспектор" в контекстном меню), перейдите на вкладку "Redux". Там вы увидите историю диспетчеризованных действий и текущее состояние:

Redux DevTools: начальное состояние приложения

Текущее состояние должно выглядеть так:

{
counter: {
value: 0
}
}

Эта структура определена опцией reducer, которую мы передали в configureStore: объект с полем counter, а редьюсер для поля counter возвращает состояние вида {value}.

Экспорт типов хранилища

Поскольку мы используем TypeScript, нам часто потребуются типы для "состояния Redux" и "функции dispatch хранилища".

Экспортируем эти типы из файла store.ts, используя оператор TypeScript typeof для выведения типов на основе определения хранилища:

src/app/store.ts
import { configureStore } from '@reduxjs/toolkit'

// omit counter slice setup

export const store = configureStore({
reducer: {
counter: counterReducer
}
})

// Infer the type of `store`
export type AppStore = typeof store
// Infer the `AppDispatch` type from the store itself
export type AppDispatch = typeof store.dispatch
// Same for the `RootState` type
export type RootState = ReturnType<typeof store.getState>

Если навести курсор на тип RootState в редакторе, вы увидите type RootState = { counter: CounterState; }. Поскольку тип автоматически выводится из определения хранилища, все будущие изменения в reducer автоматически отразятся в RootState. Это позволяет определить тип единожды, и он всегда будет актуален.

Экспорт типизированных хуков

Мы будем активно использовать хуки React-Redux useSelector и useDispatch в компонентах. Им потребуются ссылки на типы RootState и AppDispatch при каждом использовании.

Мы можем упростить использование и избежать повторного указания типов, если создадим предварительно типизированные версии этих хуков со встроенными правильными типами.

React-Redux 9.1 включает методы .withTypes(), которые применяют правильные типы к этим хукам. Мы можем экспортировать эти предварительно типизированные хуки и использовать их в остальной части приложения:

src/app/hooks.ts
// This file serves as a central hub for re-exporting pre-typed Redux hooks.
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()

На этом процесс настройки завершен. Давайте начнем создавать приложение!

Лента публикаций

Основной функцией нашего приложения социальной сети будет список публикаций. Мы добавим несколько дополнительных элементов к этой функции по мере продвижения, но для начала наша первая цель — просто отобразить список публикаций на экране.

Создание слайса публикаций

Первый шаг — создать новый Redux "слайс" (slice), который будет содержать данные наших публикаций.

«Слайс» (slice) — это коллекция логики редюсеров и действий Redux для отдельной функциональности приложения, обычно определяемая в одном файле. Название происходит от разделения корневого объекта состояния Redux на несколько «слайсов» состояния.

Как только данные публикаций попадут в хранилище Redux, мы сможем создать React-компоненты для их отображения на странице.

Внутри папки src создайте новую папку features, поместите папку posts внутрь папки features и добавьте файл postsSlice.ts.

Мы будем использовать функцию createSlice из Redux Toolkit для создания редюсера, который умеет обрабатывать данные публикаций. Редюсеры должны содержать начальные данные, чтобы хранилище Redux загрузило эти значения при запуске приложения.

Пока мы создадим массив с несколькими тестовыми объектами публикаций, чтобы начать разработку интерфейса.

Импортируем createSlice, определим начальный массив публикаций, передадим его в createSlice и экспортируем редюсер публикаций, который createSlice сгенерировал для нас:

features/posts/postsSlice.ts
import { createSlice } from '@reduxjs/toolkit'

// Define a TS type for the data we'll be using
export interface Post {
id: string
title: string
content: string
}

// Create an initial state value for the reducer, with that type
const initialState: Post[] = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]

// Create the slice and pass in the initial state
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})

// Export the generated reducer function
export default postsSlice.reducer

Каждый раз при создании нового слайса нам нужно добавить его редюсер в наше хранилище Redux. У нас уже есть созданное хранилище, но пока оно не содержит данных. Откройте app/store.ts, импортируйте функцию postsReducer, удалите весь код, связанный с counter, и обновите вызов configureStore, добавив редюсер postsReducer в поле posts:

app/store.ts
import { configureStore } from '@reduxjs/toolkit'

// Removed the `counterReducer` function, `CounterState` type, and `Action` import

import postsReducer from '@/features/posts/postsSlice'

export const store = configureStore({
reducer: {
posts: postsReducer
}
})

Это указывает Redux, что наш корневой объект состояния должен содержать поле posts, а все данные для state.posts будут обновляться функцией postsReducer при диспетчеризации действий.

Мы можем проверить работу, открыв Redux DevTools Extension и посмотрев текущее содержимое состояния:

Начальное состояние публикаций

Отображение списка публикаций

Теперь, когда у нас есть данные публикаций в хранилище, мы можем создать React-компонент для отображения их списка. Весь код, связанный с публикациями, должен находиться в папке posts, поэтому создайте файл PostsList.tsx там. (Поскольку это React-компонент на TypeScript с использованием JSX, требуется расширение .tsx для правильной компиляции TypeScript)

Чтобы отобразить список публикаций, нам нужно откуда-то получить данные. React-компоненты могут читать данные из хранилища Redux с помощью хука useSelector из библиотеки React-Redux. Написанные вами "функции-селекторы" будут вызываться со всем объектом state Redux в качестве параметра и должны возвращать конкретные данные, необходимые компоненту из хранилища.

Поскольку мы используем TypeScript, все компоненты должны использовать предварительно типизированный хук useAppSelector из src/app/hooks.ts, так как он уже содержит правильный тип RootState.

Наш начальный компонент PostsList будет читать значение state.posts из хранилища Redux, затем перебирать массив постов и отображать каждый из них на экране:

features/posts/PostsList.tsx
import { useAppSelector } from '@/app/hooks'

export const PostsList = () => {
// Select the `state.posts` value from the store into the component
const posts = useAppSelector(state => state.posts)

const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))

return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}

Затем нам нужно обновить маршрутизацию в App.tsx, чтобы вместо приветственного сообщения отображался компонент PostsList. Импортируем компонент PostsList в App.tsx и заменим приветственный текст на <PostsList />. Также обернём его в React Fragment, так как скоро добавим ещё один элемент на главную страницу:

App.tsx
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

import { Navbar } from './components/Navbar'
import { PostsList } from './features/posts/PostsList'

function App() {
return (
<Router>
<Navbar />
<div className="App">
<Routes>
<Route
path="/"
element={
<>
<PostsList />
</>
}
></Route>
</Routes>
</div>
</Router>
)
}

export default App

После добавления главная страница приложения будет выглядеть так:

Initial posts list

Прогресс! Мы добавили данные в хранилище Redux и отобразили их в React-компоненте.

Добавление новых постов

Приятно читать посты других пользователей, но нам также нужно создавать собственные. Добавим форму "Добавить новый пост" для написания и сохранения записей.

Сначала создадим пустую форму и добавим её на страницу. Затем подключим форму к хранилищу Redux, чтобы новые посты добавлялись при клике на кнопку "Сохранить пост".

Добавление формы нового поста

Создадим файл AddPostForm.tsx в папке posts. Добавим текстовое поле для заголовка и текстовую область для содержимого поста:

features/posts/AddPostForm.tsx
import React from 'react'

// TS types for the input fields
// See: https://epicreact.dev/how-to-type-a-react-form-on-submit-handler/
interface AddPostFormFields extends HTMLFormControlsCollection {
postTitle: HTMLInputElement
postContent: HTMLTextAreaElement
}
interface AddPostFormElements extends HTMLFormElement {
readonly elements: AddPostFormFields
}

export const AddPostForm = () => {
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()

const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value

console.log('Values: ', { title, content })

e.currentTarget.reset()
}

return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}

Пока здесь нет логики, связанной с Redux — добавим её далее.

В этом примере используются "неуправляемые" элементы ввода и HTML5-валидация для предотвращения отправки пустых полей. Выбор способа получения значений формы — это вопрос предпочтений в работе с React, а не особенность Redux.

Импортируем компонент в App.tsx и разместим его над компонентом <PostsList />:

App.tsx
// omit outer `<App>` definition
<Route
path="/"
element={
<>
<AddPostForm />
<PostsList />
</>
}
></Route>

Теперь форма должна отображаться на странице сразу под заголовком.

Сохранение записей постов

Теперь обновим наш слайс posts для добавления новых записей в хранилище Redux.

Наш слайс posts отвечает за все обновления данных постов. Внутри вызова createSlice есть объект reducers, который сейчас пуст. Нужно добавить в него функцию-редюсер для обработки добавления нового поста.

Внутри reducers добавим функцию postAdded, которая принимает два аргумента: текущее значение state и объект action, отправленный через dispatch. Поскольку слайс posts работает только со своими данными, аргумент state будет представлять собой массив постов (не всё состояние Redux).

Объект action будет содержать новый пост в поле action.payload. При объявлении функции-редюсера нужно указать TypeScript тип для action.payload, чтобы обеспечить корректную проверку типов при передаче аргументов и доступе к содержимому action.payload. Для этого импортируем тип PayloadAction из Redux Toolkit и объявим аргумент action как action: PayloadAction<ThePayloadTypeHere>. В данном случае — action: PayloadAction<Post>.

Фактическое обновление состояния — добавление нового объекта поста в массив state через state.push() внутри редюсера.

Предупреждение

Помните: Функции-редьюсеры Redux должны всегда создавать новое состояние иммутабельно, делая копии! Внутри createSlice() безопасно вызывать мутирующие функции, такие как Array.push(), или изменять поля объектов вроде state.someField = someValue, потому что эти мутации преобразуются в безопасные иммутабельные обновления с помощью библиотеки Immer. Но никогда не пытайтесь изменять данные вне createSlice!

Когда мы пишем функцию-редьюсер postAdded, createSlice автоматически сгенерирует функцию-"создатель действия" с тем же именем. Мы можем экспортировать этого создателя и использовать его в компонентах интерфейса для диспатчинга действия, когда пользователь нажимает "Сохранить пост".

features/posts/postsSlice.ts
// Import the `PayloadAction` TS type
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

// omit initial state

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// Declare a "case reducer" named `postAdded`.
// The type of `action.payload` will be a `Post` object.
postAdded(state, action: PayloadAction<Post>) {
// "Mutate" the existing state array, which is
// safe to do here because `createSlice` uses Immer inside.
state.push(action.payload)
}
}
})

// Export the auto-generated action creator with the same name
export const { postAdded } = postsSlice.actions

export default postsSlice.reducer

В терминологии postAdded здесь — пример "редьюсера для конкретного случая" (case reducer). Это функция-редьюсер внутри слайса, которая обрабатывает один конкретный тип действия. Концептуально это похоже на case внутри switch — "если видим этот тип действия, выполняем эту логику":

function sliceReducer(state = initialState, action) {
switch (action.type) {
case 'posts/postAdded': {
// update logic here
}
}
}

Диспатчинг действия "Добавлен пост"

Наша форма AddPostForm имеет текстовые поля и кнопку "Сохранить пост", которая вызывает обработчик отправки, но пока ничего не делает. Нужно обновить обработчик, чтобы он диспатчил создателя действия postAdded и передавал новый объект поста с заголовком и контентом, введённым пользователем.

Нашим постам также нужно поле id. Сейчас начальные тестовые посты используют произвольные числа для ID. Можно написать код, вычисляющий следующее инкрементное значение, но лучше сгенерировать случайный уникальный ID. В Redux Toolkit для этого есть функция nanoid.

Информация

Подробнее о генерации ID и диспатчинге действий — в Части 4: Использование данных Redux.

Чтобы диспатчить действия из компонента, нам нужен доступ к функции dispatch из хранилища. Её мы получаем через хук useDispatch из React-Redux. Поскольку мы используем TypeScript, импортируем useAppDispatch с корректными типами. Также импортируем в этот файл создателя действия postAdded.

Получив dispatch в компоненте, мы можем вызвать dispatch(postAdded()) в обработчике клика. Берём значения заголовка и контента из формы, генерируем новый ID и формируем объект поста для передачи в postAdded().

features/posts/AddPostForm.tsx
import React from 'react'
import { nanoid } from '@reduxjs/toolkit'

import { useAppDispatch } from '@/app/hooks'

import { type Post, postAdded } from './postsSlice'

// omit form types

export const AddPostForm = () => {
// Get the `dispatch` method from the store
const dispatch = useAppDispatch()


const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// Prevent server submission
e.preventDefault()

const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value

// Create the post object and dispatch the `postAdded` action
const newPost: Post = {
id: nanoid(),
title,
content
}
dispatch(postAdded(newPost))

e.currentTarget.reset()
}

return (
<section>
<h2>Add a New Post</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">Post Title:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>Save Post</button>
</form>
</section>
)
}

Попробуйте ввести заголовок и текст, затем нажмите "Сохранить пост". В списке постов появится новый элемент.

Поздравляем! Вы только что создали своё первое работающее приложение на React + Redux!

Вот полный цикл потока данных в Redux:

  • Наш список постов прочитал начальные данные из хранилища через useSelector и отрисовал начальный UI

  • Мы задиспатчили действие postAdded с данными нового поста

  • Редьюсер постов обработал postAdded и обновил массив постов новой записью

  • Хранилище Redux уведомило UI об изменении данных

  • Список постов прочитал обновлённый массив и перерисовался, отобразив новый пост

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

Мы можем проверить расширение Redux DevTools, чтобы увидеть отправленное действие и посмотреть, как обновилось состояние Redux в ответ на него. Если мы кликнем на запись "posts/postAdded" в списке действий, вкладка "Action" должна выглядеть так:

Содержимое действия postAdded

На вкладке "Diff" также должно отображаться, что в state.posts был добавлен один новый элемент с индексом 2.

Помните: хранилище Redux должно содержать только данные, которые считаются "глобальными" для приложения! В данном случае только компонент AddPostForm должен знать последние значения полей ввода. Даже если бы мы использовали "управляемые" поля ввода, мы бы сохраняли эти временные данные в состоянии React-компонента, а не в хранилище Redux. Когда пользователь завершит работу с формой, мы отправляем действие Redux для обновления хранилища финальными значениями.

Итоги изученного

Мы настроили основы приложения Redux: хранилище, срез с редюсерами и интерфейс для отправки действий. Вот как выглядит приложение на данный момент:

Давайте подведем итог тому, что вы узнали в этом разделе:

Краткое содержание
  • Redux-приложение имеет единое store, которое передаётся React-компонентам через <Provider>
  • Состояние Redux обновляется "функциями-редюсерами":
    • Редюсеры всегда вычисляют новое состояние иммутабельно, копируя существующие значения и модифицируя копии
    • Функция createSlice из Redux Toolkit генерирует редюсеры для "срезов", позволяя писать "мутирующий" код, который преобразуется в безопасные иммутабельные обновления
    • Эти редюсеры добавляются в поле reducer при вызове configureStore, определяя структуру данных в хранилище
  • React-компоненты читают данные из хранилища с помощью хука useSelector:
    • Селекторные функции получают весь объект state и возвращают значение
    • Селекторы перезапускаются при обновлении хранилища, и если возвращаемые данные изменились, компонент перерендеривается
  • React-компоненты отправляют действия для обновления хранилища через хук useDispatch:
    • createSlice генерирует функции-создатели действий для каждого редюсера
    • Вызов dispatch(someActionCreator()) в компоненте отправляет действие
    • Редюсеры проверяют релевантность действия и возвращают новое состояние при необходимости
    • Временные данные (например, значения полей форм) должны храниться в состоянии React-компонента или как обычные HTML-поля. Отправляйте действие Redux для обновления хранилища после завершения работы с формой
  • При использовании TypeScript в настройке приложения должны быть определены типы RootState и AppDispatch на основе хранилища, а также экспортированы предварительно типизированные версии хуков useSelector и useDispatch

Что дальше?

Теперь, когда вы знаете основы потока данных в Redux, переходите к Части 4: Использование данных в Redux, где мы добавим дополнительную функциональность в наше приложение и рассмотрим примеры работы с данными, которые уже находятся в хранилище.