Redux — одна из самых популярных библиотек для управления состоянием в приложениях. Она помогает структурировать код, делает состояние предсказуемым и значительно упрощает отладку. В этом руководстве мы разберём Redux подробно, начиная с основ и переходя к практике.

В первой части мы узнаем, зачем нужен Redux, разберём его основные принципы и изучим ключевые элементы.

Часть 1: Основы Redux

Что такое Redux и зачем он нужен?

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

Почему именно Redux?

  • Единый источник правды: Всё состояние приложения хранится в одном месте — в объекте, который называется store. Это позволяет легко контролировать, какие данные где используются.
  • Предсказуемость: Изменения состояния происходят через чётко определённые действия (actions) и чистые функции (reducers). Это делает логику приложения прозрачной.
  • Инструменты отладки: Redux DevTools — мощный инструмент, который позволяет видеть историю изменений состояния, откатываться к предыдущим состояниям и анализировать экшены.

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

Redux нужен не всегда. Вот основные сценарии:

  • Приложения со сложным состоянием. Например, если у вас есть аутентификация, данные пользователя, корзина покупок и фильтры, которые должны взаимодействовать.
  • Глобальное состояние. Когда данные нужны нескольким компонентам на разных уровнях дерева (например, текущая тема или авторизация).
  • Сложная логика обновления. Если состояние обновляется разными компонентами или взаимодействие данных между компонентами сложное.

Когда Redux не нужен?

  • Если приложение небольшое, и данные можно передать через props или useState. Например, список задач или счётчик.
  • Когда контекст React (useContext) решает задачу глобального состояния.

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

Redux основывается на трёх ключевых принципах:

Единый источник правды

Всё состояние приложения хранится в одном объекте store. Это делает управление данными централизованным и удобным.

const initialState = {
  user: { name: 'Иван', isLoggedIn: true },
  cart: [{ id: 1, name: 'Товар 1', quantity: 2 }],
  theme: 'dark',
};

Состояние только для чтения

Состояние нельзя изменить напрямую. Чтобы изменить данные, нужно отправить action — объект, описывающий, что именно должно измениться.

const action = {
  type: 'ADD_TO_CART',
  payload: { id: 2, name: 'Товар 2', quantity: 1 },
};

Изменения через чистые функции

Логика обновления состояния определяется чистыми функциями, называемыми reducers. Они принимают текущее состояние и экшен, а возвращают новое состояние.

function cartReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      return [...state, action.payload];
    default:
      return state;
  }
}

Основные элементы Redux

Чтобы лучше понять, как работает Redux, давайте разберём его основные элементы:

Store

Store — это объект, который хранит состояние вашего приложения. Он создаётся с помощью функции createStoreRedux Toolkit используется configureStore).

import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

Actions

Actions — это объекты, описывающие, что именно должно произойти. Каждый экшен содержит поле type (тип действия) и, при необходимости, дополнительные данные (payload).

const addToCart = {
  type: 'ADD_TO_CART',
  payload: { id: 2, name: 'Товар 2', quantity: 1 },
};

Reducers

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


function cartReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TO_CART':
      return [...state, action.payload];
    default:
      return state;
  }
}

Dispatch

Чтобы изменить состояние, мы используем метод dispatch. Он отправляет экшен в store, который передаёт его редьюсеру.

store.dispatch({
  type: 'ADD_TO_CART',
  payload: { id: 3, name: 'Товар 3', quantity: 1 },
});

Selectors

Selectors — это функции, которые извлекают данные из store. Они позволяют уменьшить дублирование кода и улучшить читаемость.

const getCartItems = (state) => state.cart;
const items = getCartItems(store.getState());

Часть 2: Практика с Redux Toolkit

В этой части создадим приложение для управления списком покупок. Это приложение позволит добавлять, удалять и просматривать товары в списке.

Redux Toolkit включает в себя готовые инструменты, которые упрощают создание store и работы с редьюсерами. Мы будем использовать:

  • createSlice — для определения состояния, экшенов и редьюсеров.
  • configureStore — для настройки и создания store.
  • useDispatch и useSelector — хуки для взаимодействия с Redux.

Шаг 1: Установка и настройка Redux Toolkit

Убедитесь, что Redux Toolkit и React Redux установлены:

npm install @reduxjs/toolkit react-redux

Создадим структуру проекта:

src/
  store/
    index.js       // Store
    shoppingSlice.js // Slice для списка покупок
  components/
    ShoppingList.jsx // Компонент списка
    AddItemForm.jsx  // Форма добавления
  App.jsx           // Главный компонент

Шаг 2: Настройка store

В src/store/index.js создаём store:

import { configureStore } from '@reduxjs/toolkit';
import shoppingReducer from './shoppingSlice';

const store = configureStore({
  reducer: {
    shopping: shoppingReducer,
  },
});

export default store;

Здесь:

  • Мы используем configureStore, чтобы создать store.
  • Добавляем редьюсер для управления списком покупок.

Шаг 3: Создание shoppingSlice

В src/store/shoppingSlice.js создаём slice:

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

const shoppingSlice = createSlice({
  name: 'shopping',
  initialState: [],
  reducers: {
    addItem: (state, action) => {
      state.push(action.payload);
    },
    removeItem: (state, action) => {
      return state.filter((_, index) => index !== action.payload);
    },
  },
});

export const { addItem, removeItem } = shoppingSlice.actions;
export default shoppingSlice.reducer;

Здесь:

  • createSlice автоматически создаёт редьюсер и экшены.
  • initialState — начальное состояние (пустой массив для списка покупок).
  • addItem — добавляет элемент в массив.
  • removeItem — удаляет элемент по индексу.

Шаг 4: Подключение Redux к React

В src/index.js оборачиваем приложение в Provider:

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')
);

Шаг 5: Компоненты приложения

Форма добавления товара

В src/components/AddItemForm.jsx создаём форму для добавления товаров:

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addItem } from '../store/shoppingSlice';

function AddItemForm() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch(addItem(text)); // Отправляем экшен для добавления
      setText(''); // Очищаем поле ввода
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Добавить товар"
        style={{ padding: '10px', marginRight: '10px' }}
      />
      <button type="submit" style={{ padding: '10px' }}>
        Добавить
      </button>
    </form>
  );
}

export default AddItemForm;

Список покупок

В src/components/ShoppingList.jsx создаём список товаров:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { removeItem } from '../store/shoppingSlice';

function ShoppingList() {
  const items = useSelector((state) => state.shopping);
  const dispatch = useDispatch();

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {items.map((item, index) => (
        <li
          key={index}
          style={{
            marginBottom: '10px',
            padding: '10px',
            border: '1px solid #ddd',
            borderRadius: '5px',
          }}
        >
          {item}{' '}
          <button
            onClick={() => dispatch(removeItem(index))}
            style={{
              marginLeft: '10px',
              padding: '5px',
              backgroundColor: 'red',
              color: 'white',
              border: 'none',
              borderRadius: '5px',
              cursor: 'pointer',
            }}
          >
            Удалить
          </button>
        </li>
      ))}
    </ul>
  );
}

export default ShoppingList;

Главный компонент

В src/App.jsx объединяем всё:

import React from 'react';
import AddItemForm from './components/AddItemForm';
import ShoppingList from './components/ShoppingList';

function App() {
  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>Список покупок</h1>
      <AddItemForm />
      <ShoppingList />
    </div>
  );
}

export default App;

Шаг 6: Запуск приложения

Запустите проект:

npm start

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

Часть 3: Улучшение приложения

Теперь улучшим наше приложение, добавив дополнительные функции: сохранение данных в localStorage, валидацию и динамическую стилизацию.

Сохранение данных в localStorage

Сейчас данные списка покупок теряются при перезагрузке страницы. Мы можем исправить это, сохраняя данные в localStorage и восстанавливая их при загрузке приложения.

Обновим store/index.js, чтобы загружать данные из localStorage при инициализации:

const loadState = () => {
  const savedState = localStorage.getItem('shoppingList');
  return savedState ? JSON.parse(savedState) : [];
};

const store = configureStore({
  reducer: {
    shopping: shoppingSlice.reducer,
  },
  preloadedState: {
    shopping: loadState(),
  },
});

Подписываемся на изменения состояния, чтобы сохранять список в localStorage:

store.subscribe(() => {
  const state = store.getState();
  localStorage.setItem('shoppingList', JSON.stringify(state.shopping));
});

Теперь, даже если вы перезагрузите страницу, список товаров будет восстановлен из localStorage.

Добавление валидации

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

Изменим логику добавления товара, чтобы проверять, не пустое ли поле и нет ли уже такого товара в списке:

function AddItemForm() {
  const [text, setText] = useState('');
  const [error, setError] = useState('');
  const dispatch = useDispatch();
  const items = useSelector((state) => state.shopping);

  const handleSubmit = (e) => {
    e.preventDefault();

    // Проверяем, что поле не пустое
    if (!text.trim()) {
      setError('Введите название товара.');
      return;
    }

    // Проверяем, что товар уникален
    if (items.includes(text)) {
      setError('Товар уже есть в списке.');
      return;
    }

    dispatch(addItem(text));
    setText('');
    setError('');
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Добавить товар"
        style={{ padding: '10px', marginRight: '10px' }}
      />
      <button type="submit" style={{ padding: '10px' }}>
        Добавить
      </button>
      {error && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
    </form>
  );
}

Теперь:

  • Пользователь не сможет добавить пустой товар.
  • При попытке добавить дублирующийся товар появится сообщение об ошибке.

Динамическая стилизация

Добавим возможность помечать товары как купленные. Для этого:

  • Добавим в каждый товар свойство isPurchased.
  • Стилизуем купленные товары.

Обновление shoppingSlice

Добавим новый экшен для переключения статуса «Куплено»:

const shoppingSlice = createSlice({
  name: 'shopping',
  initialState: [],
  reducers: {
    addItem: (state, action) => {
      state.push({ name: action.payload, isPurchased: false });
    },
    removeItem: (state, action) => {
      return state.filter((_, index) => index !== action.payload);
    },
    togglePurchased: (state, action) => {
      const item = state[action.payload];
      if (item) {
        item.isPurchased = !item.isPurchased;
      }
    },
  },
});

export const { addItem, removeItem, togglePurchased } = shoppingSlice.actions;
export default shoppingSlice.reducer;

Обновление ShoppingList

Теперь мы можем отображать купленные товары с изменённым стилем:

function ShoppingList() {
  const items = useSelector((state) => state.shopping);
  const dispatch = useDispatch();

  return (
    <ul style={{ listStyle: 'none', padding: 0 }}>
      {items.map((item, index) => (
        <li
          key={index}
          style={{
            marginBottom: '10px',
            padding: '10px',
            border: '1px solid #ddd',
            borderRadius: '5px',
            backgroundColor: item.isPurchased ? '#d4edda' : '#f8d7da',
          }}
        >
          <span
            style={{
              textDecoration: item.isPurchased ? 'line-through' : 'none',
              cursor: 'pointer',
            }}
            onClick={() => dispatch(togglePurchased(index))}
          >
            {item.name}
          </span>
          <button
            onClick={() => dispatch(removeItem(index))}
            style={{
              marginLeft: '10px',
              padding: '5px',
              backgroundColor: 'red',
              color: 'white',
              border: 'none',
              borderRadius: '5px',
              cursor: 'pointer',
            }}
          >
            Удалить
          </button>
        </li>
      ))}
    </ul>
  );
}

Теперь:

  • Пользователь может кликнуть по названию товара, чтобы пометить его как купленный.
  • Купленные товары будут выделены зелёным фоном и зачёркнуты.

Итоговый код

store/index.js

import { configureStore } from '@reduxjs/toolkit';
import shoppingReducer from './shoppingSlice';

const loadState = () => {
  const savedState = localStorage.getItem('shoppingList');
  return savedState ? JSON.parse(savedState) : [];
};

const store = configureStore({
  reducer: {
    shopping: shoppingReducer,
  },
  preloadedState: {
    shopping: loadState(),
  },
});

store.subscribe(() => {
  const state = store.getState();
  localStorage.setItem('shoppingList', JSON.stringify(state.shopping));
});

export default store;

shoppingSlice.js

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

const shoppingSlice = createSlice({
  name: 'shopping',
  initialState: [],
  reducers: {
    addItem: (state, action) => {
      state.push({ name: action.payload, isPurchased: false });
    },
    removeItem: (state, action) => {
      return state.filter((_, index) => index !== action.payload);
    },
    togglePurchased: (state, action) => {
      const item = state[action.payload];
      if (item) {
        item.isPurchased = !item.isPurchased;
      }
    },
  },
});

export const { addItem, removeItem, togglePurchased } = shoppingSlice.actions;
export default shoppingSlice.reducer;

 

 

 

Комментарии

0

Без регистрации и смс