Настройка Redux Store, типизация хуков и интеграция с React на TypeScript
Настройка Redux Store, типизация хуков и интеграция с React на TypeScript
Добро пожаловать в курс Redux Toolkit и RTK Query: От основ до PRO на TypeScript. Это первая статья нашего цикла, в которой мы заложим фундамент для всех будущих приложений. Мы не просто настроим Redux, мы сделаем это правильно: с полной типизацией, использованием современных хуков и интеграцией в React-приложение.
Redux Toolkit (RTK) — это официальный, рекомендованный способ написания логики Redux. Он решает множество проблем «старого» Redux: убирает необходимость в огромном количестве шаблонного кода (boilerplate), упрощает настройку хранилища и делает работу с TypeScript интуитивно понятной.
Установка зависимостей
Прежде чем писать код, нам нужно установить необходимые пакеты. Мы предполагаем, что у вас уже развернут React-проект на TypeScript (например, через Vite или Create React App).
Выполните следующую команду в терминале:
npm install @reduxjs/toolkit react-redux
Здесь:
@reduxjs/toolkit— основная библиотека, включающая методы для создания слайсов, стора и RTK Query.react-redux— библиотека для связывания Redux с компонентами React.
Архитектура Redux Toolkit
В современном Redux мы оперируем понятием Slice (срез). Слайс — это коллекция логики редюсера и действий (actions) для одной конкретной функции вашего приложения (например, авторизация, корзина товаров или счетчик).
Шаг 1: Создание Слайса (Slice)
Создадим простую функциональность счетчика, чтобы продемонстрировать типизацию. Создайте файл src/store/features/counter/counterSlice.ts.
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// 1. Описываем интерфейс состояния
interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
// 2. Задаем начальное состояние
const initialState: CounterState = {
value: 0,
status: 'idle',
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// Redux Toolkit позволяет писать "мутирующую" логику в редюсерах.
// На самом деле он не мутирует состояние, а использует библиотеку Immer
// под капотом, которая обеспечивает неизменяемость.
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// Пример экшена с полезной нагрузкой (payload)
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
// Генерируем экшены автоматически
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Экспортируем редюсер для добавления в store
export default counterSlice.reducer;
Обратите внимание на PayloadAction<number>. Это дженерик-тип, который гарантирует, что в action.payload придет именно число, а не строка или объект. Это и есть сила TypeScript.
Шаг 2: Настройка Store
Теперь создадим само хранилище. Создайте файл src/store/store.ts.
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
// Здесь будут добавляться другие редюсеры
},
});
// Выводим типы `RootState` и `AppDispatch` из самого хранилища
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Функция configureStore автоматически настраивает инструменты разработчика (Redux DevTools) и добавляет полезные middleware (например, для проверки на случайные мутации).
Ключевой момент здесь — экспорт типов RootState и AppDispatch. Мы не пишем их вручную, мы просим TypeScript вывести их на основе созданного объекта store. Это гарантирует, что типы всегда будут актуальны при изменении редюсеров.
Шаг 3: Типизация хуков
В обычном JavaScript мы бы использовали useDispatch и useSelector напрямую из библиотеки react-redux. Однако в TypeScript это неудобно: useSelector не знает структуру нашего стейта, а useDispatch не знает о thunk-экшенах.
Лучшая практика — создать типизированные версии этих хуков. Создайте файл src/store/hooks.ts.
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Используйте эти хуки во всем приложении вместо обычных `useDispatch` и `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Теперь, когда вы будете использовать useAppSelector, TypeScript будет подсказывать вам поля вашего состояния (например, state.counter.value).
Шаг 4: Провайдер Store
Чтобы React увидел Redux, нужно обернуть приложение в компонент Provider. Обычно это делается в корневом файле, например main.tsx или index.tsx.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store/store';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
Интеграция в компонент
Теперь соберем всё вместе. Создадим компонент, который использует наши типизированные хуки и стилизован с помощью Tailwind CSS.
import React, { useState } from 'react';
import { useAppSelector, useAppDispatch } from './store/hooks';
import { increment, decrement, incrementByAmount } from './store/features/counter/counterSlice';
export default function Home() {
// Используем типизированный селектор
const count = useAppSelector((state) => state.counter.value);
// Используем типизированный диспатч
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState<string>('2');
const amountValue = Number(incrementAmount) || 0;
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-xl shadow-lg w-full max-w-md">
<h1 className="text-2xl font-bold text-center mb-6 text-gray-800">
Redux Toolkit Counter
</h1>
<div className="flex items-center justify-center space-x-6 mb-8">
<button
className="w-12 h-12 flex items-center justify-center bg-red-500 hover:bg-red-600 text-white rounded-full text-2xl transition-colors"
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className="text-4xl font-mono font-bold text-gray-700">
{count}
</span>
<button
className="w-12 h-12 flex items-center justify-center bg-green-500 hover:bg-green-600 text-white rounded-full text-2xl transition-colors"
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className="flex flex-col space-y-4">
<div className="flex space-x-4">
<input
className="flex-1 border-2 border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:border-blue-500"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
type="number"
/>
<button
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg transition-colors"
onClick={() => dispatch(incrementByAmount(amountValue))}
>
Add Amount
</button>
</div>
</div>
</div>
</div>
);
}
Разбор кода компонента
useAppSelector: Мы передаем функцию(state) => state.counter.value. Благодаря типизации, IDE знает, что уstateесть полеcounter, а у него — полеvalue. Если вы ошибетесь в названии, TypeScript немедленно подсветит ошибку.useAppDispatch: Возвращает функциюdispatch, которая принимает только валидные экшены. Если вы попробуете отправитьincrementByAmount('пять')(строку вместо числа), TypeScript укажет на несоответствие типаPayloadAction<number>.
Почему это важно?
Использование Redux Toolkit в связке с TypeScript дает нам:
- Безопасность: Вы не можете обратиться к несуществующему полю стейта.
- Скорость разработки: Автодополнение кода работает великолепно.
- Читаемость: Структура кода становится стандартизированной и понятной любому разработчику, знакомому с RTK.
В следующей статье мы углубимся в работу с асинхронными данными и рассмотрим, как RTK Query меняет правила игры при работе с API.