← React/Redux Toolkit: createSlice и configureStore#302 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Redux Toolkit: createSlice и configureStore

Почему Redux Toolkit

Redux Toolkit (RTK) — официальный способ писать Redux-код. Он решает проблемы классического Redux:

| Проблема | Классический Redux | Redux Toolkit |

|----------|-------------------|---------------|

| Много кода | action types, creators, reducers | Всё в одном slice |

| Иммутабельность | Вручную spread | Immer под капотом |

| Настройка store | Много boilerplate | configureStore |

| DevTools | Ручная настройка | Из коробки |

| Thunks | redux-thunk middleware | Встроено |

Установка

npm install @reduxjs/toolkit react-redux

> В интерактивном редакторе ниже библиотеки подключены через CDN, установка не требуется.

createSlice — сердце RTK

createSlice объединяет action types, action creators и reducer в одном месте:

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',           // Имя для action types
  initialState: { value: 0 }, // Начальное состояние
  reducers: {
    // Каждый редьюсер автоматически получает action creator
    increment: (state) => {
      state.value += 1  // Можно мутировать! Immer делает иммутабельно
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
    reset: (state) => {
      state.value = 0
    }
  }
})

// Автоматически созданные action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// Редьюсер для store
export default counterSlice.reducer

configureStore — настройка store

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
import todosReducer from './todosSlice'

const store = configureStore({
  reducer: {
    counter: counterReducer,  // state.counter
    todos: todosReducer       // state.todos
  }
  // DevTools, thunk middleware — включены по умолчанию!
})

export default store

Provider — подключение к React

import { Provider } from 'react-redux'
import store from './store'

function App() {
  return (
    <Provider store={store}>
      <Counter />
      <TodoList />
    </Provider>
  )
}

useSelector и useDispatch

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

function Counter() {
  // Читаем состояние
  const count = useSelector(state => state.counter.value)
  
  // Получаем dispatch
  const dispatch = useDispatch()

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

Immer: мутации становятся иммутабельными

RTK использует Immer под капотом. Это значит, что вы можете "мутировать" state:

// RTK с Immer — выглядит как мутация, но на самом деле иммутабельно
reducers: {
  addTodo: (state, action) => {
    state.todos.push(action.payload)  // OK!
  },
  updateTodo: (state, action) => {
    const todo = state.todos.find(t => t.id === action.payload.id)
    if (todo) {
      todo.text = action.payload.text  // OK!
    }
  }
}

// Эквивалент без Immer (классический Redux):
case 'ADD_TODO':
  return {
    ...state,
    todos: [...state.todos, action.payload]
  }

Полная структура проекта

src/
├── app/
│   └── store.js          # configureStore
├── features/
│   ├── counter/
│   │   ├── counterSlice.js
│   │   └── Counter.jsx
│   └── todos/
│       ├── todosSlice.js
│       └── TodoList.jsx
└── App.jsx

Selectors — переиспользуемые запросы к состоянию

// todosSlice.js
export const selectAllTodos = state => state.todos.items
export const selectTodoById = (state, id) => 
  state.todos.items.find(todo => todo.id === id)
export const selectCompletedTodos = state => 
  state.todos.items.filter(todo => todo.completed)
export const selectTodosCount = state => state.todos.items.length

// Использование
const todos = useSelector(selectAllTodos)
const completedTodos = useSelector(selectCompletedTodos)

Примеры

Полный пример: slice + store + компонент

// Симуляция Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
  const actions = {}
  const actionTypes = {}

  // Создаём action creators автоматически
  Object.keys(reducers).forEach(key => {
    const type = name + '/' + key
    actionTypes[key] = type
    actions[key] = (payload) => ({ type, payload })
  })

  // Создаём reducer
  function reducer(state = initialState, action) {
    const [sliceName, actionName] = action.type.split('/')
    if (sliceName === name && reducers[actionName]) {
      // Симуляция Immer — создаём копию и мутируем её
      const draft = JSON.parse(JSON.stringify(state))
      reducers[actionName](draft, action)
      return draft
    }
    return state
  }

  return { actions, reducer, name }
}

// Симуляция configureStore
function configureStore({ reducer }) {
  const reducers = reducer
  let state = {}

  // Инициализация состояния
  Object.keys(reducers).forEach(key => {
    state[key] = reducers[key](undefined, { type: '@@INIT' })
  })

  const listeners = []

  return {
    getState: () => state,
    dispatch: (action) => {
      console.log('⚡ Action:', action.type)
      Object.keys(reducers).forEach(key => {
        state[key] = reducers[key](state[key], action)
      })
      listeners.forEach(fn => fn())
    },
    subscribe: (fn) => {
      listeners.push(fn)
      return () => listeners.splice(listeners.indexOf(fn), 1)
    }
  }
}

// === Создаём slice для задач ===
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], filter: 'all' },
  reducers: {
    addTodo: (state, action) => {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      })
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    },
    removeTodo: (state, action) => {
      state.items = state.items.filter(t => t.id !== action.payload)
    },
    setFilter: (state, action) => {
      state.filter = action.payload
    }
  }
})

const { addTodo, toggleTodo, removeTodo, setFilter } = todosSlice.actions

// === Создаём store ===
const store = configureStore({
  reducer: {
    todos: todosSlice.reducer
  }
})

// Подписка на изменения
store.subscribe(() => {
  const { todos } = store.getState()
  console.log('📋 Todos:', todos.items.map(t => 
    (t.completed ? '✅' : '⬜') + ' ' + t.text
  ))
})

// === Тестируем ===
console.log('=== Redux Toolkit Demo ===\n')

store.dispatch(addTodo('Изучить createSlice'))
store.dispatch(addTodo('Настроить store'))
store.dispatch(addTodo('Подключить к React'))

const firstTodoId = store.getState().todos.items[0].id
store.dispatch(toggleTodo(firstTodoId))

console.log('\nФинальное состояние:', store.getState())

Redux Toolkit: createSlice и configureStore

Почему Redux Toolkit

Redux Toolkit (RTK) — официальный способ писать Redux-код. Он решает проблемы классического Redux:

| Проблема | Классический Redux | Redux Toolkit |

|----------|-------------------|---------------|

| Много кода | action types, creators, reducers | Всё в одном slice |

| Иммутабельность | Вручную spread | Immer под капотом |

| Настройка store | Много boilerplate | configureStore |

| DevTools | Ручная настройка | Из коробки |

| Thunks | redux-thunk middleware | Встроено |

Установка

npm install @reduxjs/toolkit react-redux

> В интерактивном редакторе ниже библиотеки подключены через CDN, установка не требуется.

createSlice — сердце RTK

createSlice объединяет action types, action creators и reducer в одном месте:

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',           // Имя для action types
  initialState: { value: 0 }, // Начальное состояние
  reducers: {
    // Каждый редьюсер автоматически получает action creator
    increment: (state) => {
      state.value += 1  // Можно мутировать! Immer делает иммутабельно
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
    reset: (state) => {
      state.value = 0
    }
  }
})

// Автоматически созданные action creators
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions

// Редьюсер для store
export default counterSlice.reducer

configureStore — настройка store

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
import todosReducer from './todosSlice'

const store = configureStore({
  reducer: {
    counter: counterReducer,  // state.counter
    todos: todosReducer       // state.todos
  }
  // DevTools, thunk middleware — включены по умолчанию!
})

export default store

Provider — подключение к React

import { Provider } from 'react-redux'
import store from './store'

function App() {
  return (
    <Provider store={store}>
      <Counter />
      <TodoList />
    </Provider>
  )
}

useSelector и useDispatch

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

function Counter() {
  // Читаем состояние
  const count = useSelector(state => state.counter.value)
  
  // Получаем dispatch
  const dispatch = useDispatch()

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

Immer: мутации становятся иммутабельными

RTK использует Immer под капотом. Это значит, что вы можете "мутировать" state:

// RTK с Immer — выглядит как мутация, но на самом деле иммутабельно
reducers: {
  addTodo: (state, action) => {
    state.todos.push(action.payload)  // OK!
  },
  updateTodo: (state, action) => {
    const todo = state.todos.find(t => t.id === action.payload.id)
    if (todo) {
      todo.text = action.payload.text  // OK!
    }
  }
}

// Эквивалент без Immer (классический Redux):
case 'ADD_TODO':
  return {
    ...state,
    todos: [...state.todos, action.payload]
  }

Полная структура проекта

src/
├── app/
│   └── store.js          # configureStore
├── features/
│   ├── counter/
│   │   ├── counterSlice.js
│   │   └── Counter.jsx
│   └── todos/
│       ├── todosSlice.js
│       └── TodoList.jsx
└── App.jsx

Selectors — переиспользуемые запросы к состоянию

// todosSlice.js
export const selectAllTodos = state => state.todos.items
export const selectTodoById = (state, id) => 
  state.todos.items.find(todo => todo.id === id)
export const selectCompletedTodos = state => 
  state.todos.items.filter(todo => todo.completed)
export const selectTodosCount = state => state.todos.items.length

// Использование
const todos = useSelector(selectAllTodos)
const completedTodos = useSelector(selectCompletedTodos)

Примеры

Полный пример: slice + store + компонент

// Симуляция Redux Toolkit createSlice
function createSlice({ name, initialState, reducers }) {
  const actions = {}
  const actionTypes = {}

  // Создаём action creators автоматически
  Object.keys(reducers).forEach(key => {
    const type = name + '/' + key
    actionTypes[key] = type
    actions[key] = (payload) => ({ type, payload })
  })

  // Создаём reducer
  function reducer(state = initialState, action) {
    const [sliceName, actionName] = action.type.split('/')
    if (sliceName === name && reducers[actionName]) {
      // Симуляция Immer — создаём копию и мутируем её
      const draft = JSON.parse(JSON.stringify(state))
      reducers[actionName](draft, action)
      return draft
    }
    return state
  }

  return { actions, reducer, name }
}

// Симуляция configureStore
function configureStore({ reducer }) {
  const reducers = reducer
  let state = {}

  // Инициализация состояния
  Object.keys(reducers).forEach(key => {
    state[key] = reducers[key](undefined, { type: '@@INIT' })
  })

  const listeners = []

  return {
    getState: () => state,
    dispatch: (action) => {
      console.log('⚡ Action:', action.type)
      Object.keys(reducers).forEach(key => {
        state[key] = reducers[key](state[key], action)
      })
      listeners.forEach(fn => fn())
    },
    subscribe: (fn) => {
      listeners.push(fn)
      return () => listeners.splice(listeners.indexOf(fn), 1)
    }
  }
}

// === Создаём slice для задач ===
const todosSlice = createSlice({
  name: 'todos',
  initialState: { items: [], filter: 'all' },
  reducers: {
    addTodo: (state, action) => {
      state.items.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      })
    },
    toggleTodo: (state, action) => {
      const todo = state.items.find(t => t.id === action.payload)
      if (todo) todo.completed = !todo.completed
    },
    removeTodo: (state, action) => {
      state.items = state.items.filter(t => t.id !== action.payload)
    },
    setFilter: (state, action) => {
      state.filter = action.payload
    }
  }
})

const { addTodo, toggleTodo, removeTodo, setFilter } = todosSlice.actions

// === Создаём store ===
const store = configureStore({
  reducer: {
    todos: todosSlice.reducer
  }
})

// Подписка на изменения
store.subscribe(() => {
  const { todos } = store.getState()
  console.log('📋 Todos:', todos.items.map(t => 
    (t.completed ? '✅' : '⬜') + ' ' + t.text
  ))
})

// === Тестируем ===
console.log('=== Redux Toolkit Demo ===\n')

store.dispatch(addTodo('Изучить createSlice'))
store.dispatch(addTodo('Настроить store'))
store.dispatch(addTodo('Подключить к React'))

const firstTodoId = store.getState().todos.items[0].id
store.dispatch(toggleTodo(firstTodoId))

console.log('\nФинальное состояние:', store.getState())

Задание

Создай todo-приложение используя паттерн Redux Toolkit. Реализуй slice с действиями addTodo, toggleTodo, removeTodo, clearCompleted. Добавь фильтрацию: all, active, completed.

Подсказка

addTodo: completed: false. toggleTodo: !todo.completed. removeTodo: t.id !== action.payload. clearCompleted: !t.completed (оставляем незавершённые).

Загружаем среду выполнения...
Загружаем AI-помощника...