Back

Понимание Redux в React: Управление состоянием как профессионал

Понимание Redux в React: Управление состоянием как профессионал

Испытываете трудности с управлением состоянием в нескольких компонентах вашего React-приложения? По мере роста вашего приложения передача пропсов через несколько уровней компонентов (prop drilling) становится громоздкой и подверженной ошибкам. Redux предлагает решение, предоставляя централизованную систему управления состоянием, которая делает ваши React-приложения более предсказуемыми и легкими в отладке.

В этом подробном руководстве вы узнаете, как Redux работает с React, когда его использовать и как эффективно внедрять его в свои проекты. Мы рассмотрим всё: от основных концепций до практической реализации с Redux Toolkit, современным способом написания кода Redux.

Ключевые моменты

  • Redux обеспечивает централизованное управление состоянием для React-приложений
  • Используйте Redux, когда в вашем приложении есть сложное общее состояние, используемое многими компонентами
  • Redux следует однонаправленному потоку данных: действия → редьюсеры → хранилище → UI
  • Redux Toolkit упрощает разработку с Redux, уменьшая шаблонный код
  • Оптимизация производительности критически важна для крупных Redux-приложений

Что такое Redux в React?

Redux — это предсказуемый контейнер состояния для JavaScript-приложений, особенно популярный с React. Он хранит всё состояние вашего приложения в едином неизменяемом объекте, называемом ""хранилищем"" (store). Этот централизованный подход к управлению состоянием решает многие распространенные проблемы в React-приложениях:

  • Prop drilling: Больше не нужно передавать состояние через множество слоев компонентов
  • Синхронизация состояния: Обеспечивает согласованное состояние между компонентами
  • Предсказуемые обновления: Изменения состояния следуют строгому однонаправленному потоку данных
  • Отладка: Делает изменения состояния отслеживаемыми и предсказуемыми

Redux не является частью самого React, но работает как дополнительная библиотека через React Redux, официальную библиотеку привязки Redux UI для React.

// Basic Redux integration with React
import { Provider } from 'react-redux';
import store from './store';

function App() {
  return (
    <Provider store={store}>
      <YourApplication />
    </Provider>
  );
}

Когда следует использовать Redux?

Не каждому React-приложению нужен Redux. Рассмотрите использование Redux, когда:

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

Для меньших приложений или компонентов с локализованным состоянием встроенное управление состоянием React (useState, useReducer и Context API) часто бывает достаточным.

Основные концепции Redux

Хранилище: Единый источник истины

Хранилище Redux содержит всё дерево состояния вашего приложения. В отличие от состояния компонентов React, которое распределено между компонентами, Redux централизует всё состояние в одном месте:

// Creating a Redux store
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

const store = configureStore({
  reducer: rootReducer
});

export default store;

Действия: Описание изменений состояния

Действия — это обычные JavaScript-объекты, которые описывают, что произошло в вашем приложении. Они являются единственным способом отправки данных в хранилище Redux:

// An action object
{
  type: 'counter/incremented',
  payload: 1
}

// Action creator function
const increment = (amount) => {
  return {
    type: 'counter/incremented',
    payload: amount
  }
}

Каждое действие должно иметь свойство type, которое описывает, какой тип действия оно представляет. Свойство payload содержит любые данные, необходимые для действия.

Редьюсеры: Чистые функции для обновления состояния

Редьюсеры — это чистые функции, которые принимают текущее состояние и действие, а затем возвращают новое состояние. Они определяют, как состояние приложения изменяется в ответ на действия:

// A simple reducer function
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'counter/incremented':
      return state + action.payload;
    case 'counter/decremented':
      return state - action.payload;
    default:
      return state;
  }
}

Редьюсеры должны:

  • Быть чистыми функциями (без побочных эффектов)
  • Никогда не изменять состояние напрямую
  • Возвращать новый объект состояния при изменениях

Однонаправленный поток данных

Redux следует строгому однонаправленному потоку данных:

  1. Вы отправляете действие из компонента
  2. Хранилище Redux передает действие редьюсеру
  3. Редьюсер создает новое состояние на основе действия
  4. Хранилище обновляет своё состояние и уведомляет все подключенные компоненты
  5. Компоненты перерисовываются с новым состоянием

Этот односторонний поток делает изменения состояния предсказуемыми и более понятными.

Интеграция React Redux

Provider: Подключение Redux к React

Компонент Provider делает хранилище Redux доступным для всех компонентов в вашем приложении:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Хуки: Доступ к Redux в компонентах

React Redux предоставляет хуки, которые позволяют вашим компонентам взаимодействовать с хранилищем Redux:

useSelector: Чтение состояния

import { useSelector } from 'react-redux';

function CounterDisplay() {
  // Select the counter value from the store
  const count = useSelector(state => state.counter.value);
  
  return <div>Current count: {count}</div>;
}

Хук useSelector:

  • Принимает функцию-селектор, которая извлекает данные из состояния хранилища
  • Перерисовывает компонент, когда выбранное состояние изменяется
  • Оптимизирует производительность, избегая ненужных перерисовок

useDispatch: Обновление состояния

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

Хук useDispatch:

  • Возвращает функцию dispatch из хранилища
  • Позволяет отправлять действия из любого компонента

Современный Redux с Redux Toolkit

Redux Toolkit — это официальный, рекомендуемый способ написания логики Redux. Он упрощает разработку Redux путем:

  • Уменьшения шаблонного кода
  • Включения полезных утилит для общих паттернов
  • Предоставления хороших настроек по умолчанию для настройки хранилища
  • Обеспечения возможности прямых мутаций состояния в редьюсерах (через Immer)

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

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
import todosReducer from './features/todos/todosSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer
  }
});

export default store;

Определение логики состояния с createSlice

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write ""mutating"" logic in reducers
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

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

Обработка асинхронных операций с createAsyncThunk

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

// Create an async thunk for fetching data
export const fetchUserData = createAsyncThunk(
  'users/fetchUserData',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return await response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserData.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUserData.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.data = action.payload;
      })
      .addCase(fetchUserData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  }
});

export default userSlice.reducer;

Пример практической реализации

Давайте создадим простое приложение-счетчик с Redux Toolkit:

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

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

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

2. Создание слайса для функции счетчика

// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

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

// Selector
export const selectCount = (state) => state.counter.value;

3. Подключение хранилища к React

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

4. Использование Redux в компоненте

// features/counter/Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  increment,
  decrement,
  incrementByAmount,
  selectCount
} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      <div>
        <button onClick={() => dispatch(decrement())}>-</button>
        <span>{count}</span>
        <button onClick={() => dispatch(increment())}>+</button>
      </div>
      <div>
        <input
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={() => 
            dispatch(incrementByAmount(Number(incrementAmount) || 0))
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

Техники оптимизации производительности

Мемоизированные селекторы с Reselect

Reselect (включен в Redux Toolkit) позволяет создавать мемоизированные функции-селекторы, которые пересчитывают результаты только при изменении входных данных:

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

// Basic selectors
const selectItems = state => state.items;
const selectFilter = state => state.filter;

// Memoized selector
export const selectFilteredItems = createSelector(
  [selectItems, selectFilter],
  (items, filter) => {
    // This calculation only runs when items or filter changes
    return items.filter(item => item.includes(filter));
  }
);

Избегание ненужных рендеров

Чтобы предотвратить перерисовку компонентов при изменении несвязанных частей состояния:

  1. Выбирайте только те конкретные данные, которые нужны вашему компоненту
  2. Используйте мемоизированные селекторы для производных данных
  3. Используйте функцию shallowEqual в качестве второго аргумента для useSelector при выборе объектов
import { useSelector, shallowEqual } from 'react-redux';

// This component will only re-render when user.name or user.email changes
function UserInfo() {
  const { name, email } = useSelector(state => ({
    name: state.user.name,
    email: state.user.email
  }), shallowEqual);
  
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
}

Redux в сравнении с другими решениями для управления состоянием

Redux vs. Context API

Context API:

  • Встроен в React
  • Проще для малых и средних приложений
  • Не требуются дополнительные библиотеки
  • Меньше шаблонного кода
  • Ограниченные инструменты отладки

Redux:

  • Более мощный для сложной логики состояния
  • Лучшая производительность для частых обновлений
  • Отличные инструменты отладки с Redux DevTools
  • Поддержка middleware для побочных эффектов
  • Более структурированный подход к управлению состоянием

Redux vs. Zustand/Recoil/MobX

Рассмотрите эти альтернативы, когда:

  • Вам нужен более простой API с меньшим количеством шаблонного кода (Zustand)
  • Вы хотите управление состоянием на основе атомов (Recoil)
  • Вы предпочитаете более объектно-ориентированный, реактивный подход (MobX)

Redux по-прежнему лучший выбор, когда вам нужны:

  • Зрелое, проверенное в боях решение
  • Отличные возможности отладки
  • Большая экосистема middleware и расширений
  • Предсказуемое управление состоянием для сложных приложений

Заключение

Redux предоставляет мощное и предсказуемое решение для управления состоянием в React-приложениях. Хотя он вносит некоторую сложность и шаблонный код, Redux Toolkit значительно упрощает опыт разработки. Для сложных приложений с общим состоянием между многими компонентами Redux предлагает явные преимущества с точки зрения поддерживаемости, отладки и предсказуемости.

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

Часто задаваемые вопросы

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

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

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

Используйте middleware, такие как Redux Thunk (включен в Redux Toolkit) или Redux Saga для более сложных асинхронных потоков. createAsyncThunk из Redux Toolkit делает асинхронные операции понятными.

Да. Хотя хуки, такие как useReducer и useContext, могут справляться с более простыми потребностями управления состоянием, Redux по-прежнему превосходит в управлении сложным состоянием приложения, особенно в больших командах.

Используйте расширение Redux DevTools для проверки состояния, действий и изменений состояния с течением времени. Это одна из самых мощных функций Redux.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers