【问题标题】:Is there a simple idiomatic way of updating key-value states in Redux reducers是否有一种简单的惯用方式来更新 Redux 减速器中的键值状态
【发布时间】:2021-07-20 17:48:50
【问题描述】:

如果一个 redux 应用有一个状态切片,其中包含对象的键值存储,那么更新它的正确模式如下所示:

return {
    ...state,
    [id]: {
        ...state[id],
        valueIWantToChange: 'someValue',
    },
}

这是由于 redux 的不变性和需要复制所有数据。

对于已知键的嵌套化简器,使用combineReducers 允许指定嵌套的状态位,它们有自己的独立代码,从而简化它。

我知道没有办法简化为键值对象编写 reducer。在像上面例子这样的状态中,每个 reducer 都需要有使用扩展运算符复制状态的样板代码,然后是我们要更新的特定 id,然后是 id 内部的状态,以及最后我们可以更新状态中的值。

我发现这写起来非常乏味,并且生成的代码不必要地臃肿,尤其是与像 Vue 这样的可变框架相比,后者只需编写 state[id].valueIWantToChange = 'someValue';

是否有任何现有的解决方案或其他方法可以使这种类型的 reducer 更易于编写?

【问题讨论】:

标签: reactjs redux


【解决方案1】:

您可能正在寻找官方的 Redux Toolkit,这是官方推荐的编写任何 Redux 逻辑的方法(现在已经两年了),并带来了一个createReducercreateSlice api 允许您只需在这些 reducer 中编写变异语句即可。

createSlice 还会自动生成动作创建器并使类型字符串常量过时。

这样的“切片”看起来像这样:


const initialState = []

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    todoAdded(state, action) {
      // ✅ This "mutating" code is okay inside of createSlice!
      state.push(action.payload)
    },
    todoToggled(state, action) {
      const todo = state.find(todo => todo.id === action.payload)
      todo.completed = !todo.completed
    },
    todosLoading(state, action) {
      // you can still use old-style immutable updates if you want to
      return {
        ...state,
        status: 'loading'
      }
    }
  }
})

// autogenerated action creators
export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer

如需快速概览,请查看the last chapter of the "Redux Fundamentals" tutorial

如需更深入的介绍,请阅读full "Redux Essentials" tutorial

【讨论】:

  • 这真的很好,来自您提供的基本链接:“Immer 跟踪您尝试进行的所有更改,然后使用该更改列表返回一个安全、不可变的更新值”(。 ..) “你只能在 Redux Toolkit 的 createSlicecreateReducer 中编写“变异”逻辑,因为它们在里面使用了 Immer!”
【解决方案2】:

如果您不想添加依赖项(某些项目无法承受),那么对于更轻量级的解决方案,我选择实现只需要几行代码的简单帮助程序。 This article 帮助我找到了简化一切的食物起点。

这里是一些示例 TypeScript 代码,您可以将其放入帮助文件中,然后用于更简洁地编写操作/reducer,而无需添加大量依赖项。

import { AnyAction } from 'redux'

// Helpers for creating actions, can use version with payload if all your actions have a payload parameter, or a more generic one

export type ActionFunction = (payload: any) => AnyAction
export type FunctionWithString<T extends Function> = T & string
export interface ActionWithPayload<T = any> extends AnyAction {
  payload: T
}
export type ActionCreator<T = any> = FunctionWithString<(payload: T) => ActionWithPayload<T>>

export function createPayloadAction<T>(name: string): ActionCreator<T> {
  const actionCreator = (payload: T) => ({
    type: name,
    payload,
  })
  actionCreator.toString = () => name
  return (actionCreator as any) as ActionCreator<T>
}

export function createAction<T>(name: string, actionFunction: ActionFunction): ActionCreator<T> {
  const actionCreator = (payload: T) => ({
    type: name,
    payload: actionFunction(payload),
  })
  actionCreator.toString = () => name
  return (actionCreator as any) as ActionCreator<T>
}

// The shape of your reducer functions, makes it easier to deal with having all reducers deal with the same payload-based action format
export type MyReducer<S, A = any> = (state: S, payload: A) => S

export interface ReducersMap<S> {
  [key: string]: MyReducer<S>
}

export function createReducer<S>(reducers: ReducersMap<S>, defaultState: S) {
  return (state: S, action: ActionWithPayload) => {
    if (!state) {
      return defaultState
    }
    const reducer = reducers[action.type]

    if (reducer) {
      return reducer(state, action.payload)
    }

    return state
  }
}


// Common use case helpers for reducers

type KeyValueState<T> = { [key: string]: T }
// Updates a specific value key in a reducer that is shaped like a key-value object
export function keyValueUpdateReducer<T>(
  state: KeyValueState<T>,
  id: string,
  value: Partial<T>,
): KeyValueState<T> {
  return {
    ...state,
    [id]: {
      ...state[id],
      ...value,
    },
  }
}

// Update a specific value in a reducer that is shaped like a key-value object
export function keyValueSetReducer<T>(
  state: KeyValueState<T>,
  id: string,
  value: T,
): KeyValueState<T> {
  return {
    ...state,
    [id]: value,
  }
}

使用示例:

action

import { createPayloadAction } from './helpers'

export interface SetAThingPayload {
  id: string
  thing: any
}
export const setAThing = createPayloadAction<SetAThingPayload('SET_A_THING')

reducer

import { createReducer, keyValueSetReducer, keyValueUpdateReducer } from './helpers'
import { setAThing } from './actions'

export const thingsReducer = createReducer<MyAppState>(
  {
    [setAThing]: (state, { id, thing }) => keyValueSetReducer(state, id, thing),
  },
  {},
)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-10
    • 1970-01-01
    • 2023-02-01
    相关资源
    最近更新 更多