Урок 7: Чтение состояния и диспатч экшенов в React-компонентах (`useSelector`, `useDispatch`)

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

Сегодня мы наконец-то заставим наш интерфейс «ожить», научив его читать данные из глобального хранилища и реагировать на действия пользователя, отправляя экшены. Мы перейдем от теории к самой что ни на есть практике. Если раньше наш Store был просто тихим складом данных, то сегодня мы установим в него двери, окна и пульт управления,  это хуки useSelector и useDispatch.

Прощай useState, здравствуй глобальное состояние

Давайте на секунду вернемся к тому с чего мы начинали. Помните нашу проблему «prop drilling»? Когда на небольшом уроке мы передавали состояние и функцию его обновления через десяток компонентов, это выглядело как забавное упражнение. Но в реальном приложении это быстро превращается в кошмар поддержки и источник ошибок.

Локальное состояние с помощью useState это прекрасный инструмент. Я до сих пор использую его для истинно локальных вещей. Окрыто ли модальное окно, значение в поле ввода перед сабмитом, состояние загрузки конкретной кнопки. Оно идеально для данных, которые живут в рамках одного компонента и никого больше не волнуют.

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

Сегодня мы научимся не просто хранить данные в Store, а подключать к ним любые компоненты, минуя все промежуточные звенья. Мы будем делать это с помощью двух главных инструментов, которые предоставляет нам библиотека react-redux,  хуков useSelector и useDispatch. Это мост между миром React и миром Redux.

В моем блоге вы также найдете полный цикл уроков по React для начинающих, где мы детально разбираем все ключевые концепции: от JSX и функциональных компонентов с хуками (useState, useEffect) до более сложных тем, таких как оптимизация рендеров и создание собственных хуков.

useSelector

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

Представьте, что Store это огромная библиотека. В ней много стеллажей (слайсов), стеллаж с книгами о счетчике (counter), стеллаж со списком задач (todos), стеллаж с пользователями (users). Хук useSelector это библиотекарь, которому вы даете точный адрес нужной вам книги (данных) и он мгновенно ее находит и приносит вам.

Вот как выглядит его использование на практике:

javascript
import { useSelector } from 'react-redux';

const MyComponent = () => {
  // Извлекаем значение счетчика из состояния
  const count = useSelector((state) => state.counter.value);

  return <div>Текущее значение счетчика: {count}</div>;
};

Давайте разберем эту строчку по косточкам. Мы вызываем хук useSelector и передаем ему функцию-селектор. Эта функция принимает всё состояние приложения (целый state) в качестве аргумента и возвращает из него именно тот кусочек, который нужен компоненту. В нашем случае, мы говорим: «Эй, хранилище, дай мне свойство value, которое лежит внутри слайса counter».

Что происходит под капотом? useSelector подписывает ваш компонент на обновления Store. Каждый раз, когда состояние в Store изменяется (например, после диспатча какого-либо экшена), useSelector выполняет вашу функцию-селектор заново. Если возвращаемое значение изменилось по сравнению с предыдущим разом (сравнение происходит по строгому равенству ===), компонент будет перерендерен с новыми данными. Это невероятно эффективно! Компонент будет перерисовываться только тогда, когда нужные ему данные действительно поменялись, даже если в других частях Store вовсю кипит работа.

useDispatch

Если useSelector это наш считыватель, то useDispatch это наш отправитель команд. Этот хук возвращает нам ссылку на функцию dispatch из нашего Store. Помните нашу схему потока данных? UI -> Dispatch(Action) -> Reducer -> Store -> UIuseDispatch дает нам ту самую функцию dispatch, которая запускает весь этот конвейер.

Вот как это выглядит:

javascript
import { useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice'; // Импортируем наши экшены

const MyComponent = () => {
  // Получаем функцию dispatch
  const dispatch = useDispatch();

  const handleIncrement = () => {
    // Диспатчим экшен увеличения счетчика
    dispatch(increment());
  };

  const handleDecrement = () => {
    // Диспатчим экшен уменьшения счетчика
    dispatch(decrement());
  };

  return (
    <div>
      <button onClick={handleDecrement}>-</button>
      <button onClick={handleIncrement}>+</button>
    </div>
  );
};

Обратите внимание на красоту Redux Toolkit! Нам больше не нужно вручную создавать объекты экшенов с полем type. Мы импортируем готовые функции-создатели экшенов (action creators) — increment и decrement из нашего слайса и просто передаем их в dispatch. Функция increment() при вызове сама возвращает правильный объект экшена, например, { type: 'counter/increment' }. Это избавляет нас от тонн шаблонного кода и возможных опечаток.

Пишем компонент счетчика на Redux

Давайте на практике создадим тот самый классический компонент счетчика, но теперь на полной мощности Redux Toolkit. Предположим, что у нас уже есть слайс для счетчика, созданный с помощью createSlice, как мы это делали в Уроке 5.

1. Наш слайс счетчика (counterSlice.js):

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

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  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;

2. Наш Store (store.js) уже подключен к counterSlice.reducer и обернут в <Provider> в main.jsx, как мы сделали в Уроке 6.

3. А теперь сам звездный компонент (Counter.jsx):

javascript
import React, { useState } from 'react';
// Импортируем оба хука
import { useSelector, useDispatch } from 'react-redux';
// Импортируем наши экшены из слайса
import { increment, decrement, incrementByAmount } from './counterSlice';

export function Counter() {
  // Используем useSelector для получения значения счетчика
  const count = useSelector((state) => state.counter.value);
  
  // Используем useDispatch для получения функции dispatch
  const dispatch = useDispatch();

  // Локальное состояние для поля ввода (идеальный пример для useState!)
  const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      {/* Отображаем значение из Store */}
      <h1>Счетчик: {count}</h1>
      
      {/* Кнопки, которые диспатчат экшены */}
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      
      <br />
      <br />
      
      {/* Поле для ввода числа и кнопка для увеличения на это число */}
      <input
        value={incrementAmount}
        onChange={(e) => setIncrementAmount(e.target.value)}
      />
      <button
        onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
      >
        Увеличить на значение
      </button>
    </div>
  );
}

Запустите ваше приложение. Вы должны увидеть работающий счетчик. Нажатие на «+» диспатчит экшен increment, который пройдет через редьюсер, обновит состояние в Store, а хук useSelector в компоненте Counter «узнает» об этом и заставит компонент перерендериться с новым значением count.

Глобальное состояние count управляется Redux, а локальное состояние incrementAmount для поля ввода идеально живет в useState. Мы используем правильный инструмент для каждой задачи.

Важные нюансы

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

  1. Селекторы стоит выносить наружу. В нашем примере мы написали селектор прямо в компоненте: (state) => state.counter.value. Это нормально для маленьких приложений. Но что если структура состояния изменится? Придется менять код во всех компонентах. Гораздо лучше выносить селекторы в файлы слайсов. Мы подробнее поговорим об этом в следующем уроке, но уже сейчас вы можете сделать так:

    javascript
    // В counterSlice.js
    export const selectCount = (state) => state.counter.value;
    
    // В компоненте
    import { selectCount } from './counterSlice';
    const count = useSelector(selectCount);
  2. useSelector и сравнение объектов. Помните, что useSelector перерисовывает компонент, если результат селектора изменился (по ===). Если ваш селектор возвращает новый объект или массив, компонент будет перерисовываться при любом изменении в Store, даже если данные по факту те же.

    javascript
    // ПЛОХО - всегда возвращает новый массив
    const badTodos = useSelector((state) => state.todos.map(todo => todo));
    
    // ХОРОШО - возвращает конкретную часть состояния
    const goodTodos = useSelector((state) => state.todos.items);

    Для сложных вычислений используйте мемоизированные селекторы с createSelector (тема следующего урока).

  3. Диспатч константа. Функция dispatch, которую возвращает хук useDispatch, является стабильной и не меняется между рендерами. Ее можно безопасно включать в зависимости useEffect или useCallback, не вызывая лишних ререндеров.

Практические задачи для закрепления

Чтобы знания улеглись прочнее, я всегда советую не просто читать, а делать. Вот несколько задач для вас.

Задача 1: Базовый счетчик

Разверните новый проект React + Vite, установите RTK и react-redux и с нуля повторите весь пример выше. Создайте слайс, настройте Store, создайте и отрендерьте компонент Counter. Убедитесь, что все работает.

Задача 2: Простой список дел

Давайте усложним. Создайте новый слайс todos с начальным состоянием { items: [] }. Добавьте редьюсеры addTodo (принимает text) и removeTodo (принимает id). Не забудьте использовать prepare для автоматической генерации id (как мы обсуждали в плане Урока 8).

Затем создайте компонент TodoList:

  • Используйте useSelector для получения массива todos.

  • Выведите этот список на экран (просто текст каждого todo).

  • Добавьте кнопку «Удалить» рядом с каждым todo, которая при нажатии диспатчит экшен removeTodo с соответствующим id.

  • Создайте компонент AddTodo с полем ввода и кнопкой «Добавить». По нажатию на кнопку диспатчится экшен addTodo с тектом из поля ввода.

Задача 3: Связываем компоненты

Покажите, как Redux решает проблему «prop drilling». Создайте компонент Header, который будет отображать количество задач в списке (просто todos.items.length). Разместите этот компонент Header вверху вашего приложения, не передавая ему никаких пропсов! Убедитесь, что при добавлении или удалении задачи цифра в хедере автоматически обновляется. Почувствуйте мощь глобального состояния!

Итоги и взгляд вперед

Вы только что сделали один из самых основных шагов в освоении Redux. Вы научились напрямую подключать ваши React-компоненты к глобальному состоянию. Хуки useSelector и useDispatch это ваш основной инструмент для взаимодействия с Store на протяжении всего времени разработки.

Теперь ваше приложение обрело настоящую динамику. Данные текут предсказуемо: пользовательское действие -> dispatch -> экшен -> редьюсер -> обновление Store -> перерисовка подписанных компонентов. Эта односторонняя архитектура данных, сердце Redux. И она обеспечивает предсказуемость и удобство отладки.

В следующем уроке мы изучим тему селекторов и подготовки полезной нагрузки (prepare). Мы научимся писать более эффективные и переиспользуемые селекторы, а также автоматизировать рутинные задачи, вроде генерации ID для новых задач. Это сделает наш код гораздо чище.

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

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий