Начальная настройка Redux Toolkit с Typescript без головной боли

step-by-step гайд по настройке RTK c TS

Ну серьезно, документацию к Redux Toolkit будто в Гестапо писали 🤮 Чтобы реже её открывать, сам раскидал по порядку все шаги по настройке стора в новых проектах Vite. Да, я знаю что есть шаблон vite-template-redux, но он тоже какой-то убогий и перегруженный - быстрее настроишь все сам, чем удалишь из него лишний код.

Шаг 1. Создаем новое React приложение с помощью Vite

pnpm create vite my-react-app --template react-ts
cd ./my-react-app
pnpm install

Шаг 2. Устанавливаем redux-toolkit и react-redux

pnpm install @reduxjs/toolkit react-redux

И проверяем, что наше приложение успешно запускается:

pnpm run dev

Шаг 3. Создаем store и типизируем RootState

Обычно для Redux я создаю в проекте отдельную папку store и в нее добавляю index.ts (тут уже на вкус и цвет):

📄 src/store/index.ts

import { configureStore } from '@reduxjs/toolkit'

export const store = configureStore({
  reducer: {},
})

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Шаг 4. Создаем типизированные обертки для хуков useDispatch и useSelector

Эти обертки над useDispatch и useSelector помогут нам уменьшить количество бойлерплейта (больше не придется постоянно писать (state: RootState) и импортировать AppDispatch при каждом вызове dispatch). Файл hooks.ts я обычно создаю прямо в папке "store", так как эти хуки специфичны для Redux.

📄 src/store/hooks.ts

import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'

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

Шаг 5. Добавляем <Provider> в main.tsx

Импортируем Redux Store, который мы только что создали, и оборачиваем <App> в <Provider> с пропсом "store":

📄 src/main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { store } from './store'
import { Provider } from 'react-redux'

createRoot(document.getElementById('root')!).render(  
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>,
)

Шаг 6. Создаем slice и добавляем reducer в store

Slice для компонента счетчика - будем использовать его как пример для создания компонентов в дальнейшем:

📄 src/components/counter/counterSlice.ts

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

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

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

Добавляем reducer в store:

📄 src/store/index.ts

import { configureStore } from '@reduxjs/toolkit'
import { counterSlice } from '../components/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer
  },
})
                                    
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

Шаг 7. Используем state из Redux Store в компоненте

📄 src/components/counter/Counter.tsx

import { decrement, increment } from './counterSlice'
import { useAppDispatch, useAppSelector } from '../../store/hooks'

export const Counter = () => {
    const count = useAppSelector((state) => state.counter.value)
    const dispatch = useAppDispatch()

    return (
        <div>
            <div>
                <button onClick={() => dispatch(increment())}>
                    Increment
                </button>
                <span>{count}</span>
                <button onClick={() => dispatch(decrement())}>
                    Decrement
                </button>
            </div>
        </div>
    )
}

export default Counter;

Теперь можно импортировать этот компонент в App.ts и аналогично создавать новые компоненты, использующие Redux Store.