🦆Redux toolkit

1) Введение

🔗 Официальная документация

Redux Toolkit - это набор инструментов, разработанный командой Redux для упрощения и ускорения процесса разработки приложений на React с использованием Redux. Он предоставляет удобные абстракции над стандартным Redux API, которые позволяют сократить количество бойлерплейта, необходимого для настройки хранилища Redux и управления его состоянием.

Одной из основных проблем, которую решает Redux Toolkit, является упрощение процесса создания Redux-хранилища и управления его состоянием. Вместо того, чтобы создавать отдельные действия (actions), редьюсеры (reducers) и селекторы (selectors) вручную, Redux Toolkit предоставляет готовые инструменты для автоматической генерации всех этих элементов, используя более простой и интуитивно понятный API.

Кроме того, Redux Toolkit также предоставляет множество дополнительных функций, таких как middleware, thunk-функции и immer, которые помогают улучшить производительность, расширяемость и поддерживаемость кода, написанного с использованием Redux.

2) Добавляем в существующий проект redux toolkit

https://redux-toolkit.js.org/introduction/getting-started#installation

3) Изменения в store.ts

Store в RTK создается при помощи функции configureStore

configureStore - это функция, предоставляемая Redux Toolkit, которая делает настройку Redux store (хранения состояния приложения). Она облегчает создание store и добавляет некоторые полезные функции по умолчанию.

Например, configureStore автоматически включает проверку типов данных (type checking) с помощью пакета Redux Toolkit "redux-immutable-state-invariant". Это означает, что если вы случайно измените неизменяемое состояние в своем приложении, Redux Toolkit выдаст предупреждение об этом.

Также configureStore позволяет вам настраивать middleware (промежуточное программное обеспечение), которое позволяет вам добавлять дополнительную функциональность к вашему приложению, например, логирование или обработку ошибок.

Кроме того, configureStore автоматически включает Redux DevTools Extension, которое позволяет вам отслеживать состояние вашего приложения и действия пользователя в браузере.

Таким образом, configureStore облегчает настройку Redux store в вашем приложении и добавляет множество полезных функций по умолчанию, которые могут помочь улучшить производительность и надежность вашего приложения.

В инициализационной конфигурации store уже создан, поэтому летим дальше 🚀

3.1) Теперь создадим store согласно документации @reduxjs/toolkit 3.2) Изменим немного типизацию стейта всего приложения, чтобы соответствовать документации

export type AppRootStateType = ReturnType<typeof store.getState>

3.3) thunk middleware идет по умолчанию, поэтому его не обязательно добавлять. Но если / когда понадобится делаем это следующим образом https://redux-toolkit.js.org/usage/usage-with-typescript#correct-typings-for-the-dispatch-type https://redux-toolkit.js.org/api/getDefaultMiddleware#intended-usage

store.ts
import thunkMiddleware, { ThunkAction, ThunkDispatch } from "redux-thunk";

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().prepend(thunkMiddleware)
})

4) Reducer / slice / createSlice

Теперь давайте научимся создавать редьюсеры в RTK.

В принципе можно создать редьюсер через createReducer, но это применяется редко на практике, поэтому давайте сразу будем делать создание редьюсеров через createSlice

createSlice - это функция, которая позволяет вам быстро создавать "slice" (часть) вашего глобального состояния в Redux.

"Slice" - это кусок вашего хранилища (store), который содержит данные и функции для их изменения. Например, если у вас есть приложение с заметками, то "slice" может содержать все заметки и функции для добавления/удаления заметок.

createSlice позволяет вам определить начальное состояние "slice" и функции, называемые "reducers", которые изменяют состояние. Кроме того, она автоматически генерирует действия (actions) для каждого редьюсера, которые вы можете использовать в своем приложении.

Например, если вы хотите создать "slice" для списка заметок, вы можете использовать createSlice для определения начального состояния (например, пустого массива заметок) и редьюсеров, таких как addNote и deleteNote, которые будут добавлять или удалять заметки из списка. Затем вы можете использовать созданные действия (actions), такие как "addNote" или "deleteNote", в своем компоненте для обновления списка заметок.

В целом, createSlice упрощает процесс создания "slice" в Redux и сокращает количество кода, необходимого для создания начального состояния, редьюсеров и действий. Это может помочь ускорить разработку вашего приложения и улучшить его производительность.

4.1) На основании примера из документации перепишем authReducer на slice

auth-reducer.ts
const initialState = {
   isLoggedIn: false
}

export const authReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
   switch (action.type) {
       case 'login/SET-IS-LOGGED-IN':
           return {
             ...state,
             isLoggedIn: action.value}
       default:
           return state
   }
}

// actions
export const setIsLoggedInAC = (value: boolean) => ({type: 'login/SET-IS-LOGGED-IN', value} as const)

// types
type ActionsType = ReturnType<typeof setIsLoggedInAC>
type InitialStateType = typeof initialState

4.2) По аналогии переводим app-reducer.ts

⌚Самостоятельная работа. Начинаем самостоятельно, с преподавателем продолжаем

❗Т.к. экшены с app используются во многих санка / тестах / утилитных функциях, то рефакторинга будет много. Но ничего страшного, проделываем, набиваем руку. В любом случае сейчас или потом рефакторить придется ❗Если мы не выносим стейт в slice, то у нас будут проблемы с тестами, т.к. в тестах мы используем такой же стейт, но если мы допустим в нем ошибку, то TS не ругнется. Чтобы решить эту проблему и не выносить отдельно стейт напишем следующую типизацию export type AppInitialStateType = ReturnType<typeof slice.getInitialState>

4.3) По аналогии переводим todolists-reducer.ts

Например splice, push, pop, unshift...

Т.к. в тудулисте будут не совсем тривиальное изменение стейта, держите подсказки

todolists.reducer.ts
addTodolist: (state, action: PayloadAction<{ todolist: TodolistType }>) => {
  const newTodolist: TodolistDomainType = {...action.payload.todolist, filter: 'all', entityStatus: 'idle'}
  state.unshift(newTodolist)
},
changeTodolistTitle: (state, action: PayloadAction<{ id: string, title: string }>) => {
  // 1 variant
  const index = state.findIndex((todo) => todo.id === action.payload.id)
  if (index !== -1) state[index].title = action.payload.title
  
  // 2 variant
  const todo = state.find((todo) => todo.id === action.payload.id)
  if (todo) {
     todo.title = action.payload.title
  }
},
setTodolists: (state, action: PayloadAction<{ todolists: TodolistType[] }>) => {
  // 1 variant
  action.payload.todolists.forEach((tl) => {
      state.push({ ...tl, filter: "all", entityStatus: "idle" })
  })
  
  // 2 variant
  return action.payload.todolists.map(tl => ({...tl, filter: 'all', entityStatus: 'idle'}))
},

❗ Рассказать что менять весь стейт напрямую запрещено

Распространенная ошибка заключается в попытке напрямую присвоить state = someValue. Это не сработает! Это лишь указывает локальную переменную state на другую ссылку. Таким образом, не происходит ни мутации существующего объекта/массива состояния в памяти, ни возвращения совершенно нового значения, поэтому Immer не вносит никаких реальных изменений.

❗ Рассказать как вывести в консоль state

❗ Чтобы протестировать работоспособность:

  • todolists-reducers в tasks-reducer.ts в ActionType добавим any

  • в компоненте Todolist сделаем проверку на undefined c помощью оператора ?

4.4) По аналогии переводим tasks-reducer.ts

  • не самые проcтые изменения в редьюсерах и для того чтобы переписать некоторые кейсы нам понадобятся знания об extraReducers ⬇️

5.1) Когда нам необходимо обработать case, который был создан в другом slice (т.к. не принадлежит текущему), тогда нам необходимо в функцию createSlice добавить свойство extraReducers

Пример. Мы создали todolists-reducer с помощью createSlice, но когда мы добавляем тудулист, то нам необходимо в task-reducer написать case, который будет добавлять пустой массив тасок в только что созданный тудулист. Т.е. как раз таки и получается, что мы будем task-reducer обрабатывать case, который создан в todolists-reducer

Подсказка

tasks.reducer.ts
reducers: {
  removeTask: (state, action: PayloadAction<{ taskId: string, todolistId: string }>) => {
    const tasks = state[action.payload.todolistId]
    const index = tasks.findIndex(t => t.id === action.payload.taskId)
    if (index !== -1) tasks.splice(index, 1)
  },
  addTask: (state, action: PayloadAction<{ task: TaskType }>) => {
    const tasks = state[action.payload.task.todoListId]
    tasks.unshift(action.payload.task)
  },
  updateTask: (state, action: PayloadAction<{
	taskId: string,
	model: UpdateDomainTaskModelType,
	todolistId: string
  }>) => {
	const tasks = state[action.payload.todolistId]
	const index = tasks.findIndex(t => t.id === action.payload.taskId)
	if (index !== -1) {
	  tasks[index] = {...tasks[index], ...action.payload.model}
	}
  },
  setTasks: (state, action: PayloadAction<{ tasks: Array<TaskType>, todolistId: string }>) => {
   state[action.payload.todolistId] = action.payload.tasks
  },
},
extraReducers: builder => {
  builder
	.addCase(todolistsActions.addTodolist, (state, action) => {
	  state[action.payload.todolist.id] = []
	})
	.addCase(todolistsActions.removeTodolist, (state, action) => {
       	  delete state[action.payload.id]
	})
	.addCase(todolistsActions.setTodolists, (state, action) => {
	  action.payload.todolists.forEach((tl) => {
	    state[tl.id] = []
	  })
	})
}

5.2) синтаксис extrarReducer

  • "builder callback" notation (рекомендуемый способ работы с TS)

  • ❌ "map object" notation (удален с выходм версии 2.0)

5.3) Builder предоставляет 3 метода https://redux-toolkit.js.org/api/createReducer#usage-with-the-builder-callback-notation

  • addCase

  • addMatcher

  • addDefaultCase

Last updated

Was this helpful?