Redux: 更新嵌套实体时如何削减样板?

创建于 2015-11-03  ·  32评论  ·  资料来源: reduxjs/redux

所以我的状态下有这个嵌套结构

state = {
   plans: [
    {title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
    {title: 'B', exercises: [{title: 'exe5'}, {title: 'exe1'},{title: 'exe2'}]}
   ]
}

我正在尝试创建一个不会改变先前状态的reduce,但是到了某种程度,我花了更多时间弄清楚如何执行此操作,然后对应用程序的其余部分进行编码,这令人沮丧。

例如,如果我想添加一个新的空白练习或更新一个现有的空白练习,则可以对数据进行变异:

state.plans[planIdx].exercises.push({})

state.plans[planIdx].exercises[exerciseIdx] = exercise

但是在这种嵌套结构中做同样的事情的最佳方法是什么? 我已经阅读了Redux文档以及疑难解答部分,但是我得到的最远的是更新计划,我将在其中执行以下操作:

case 'UPDATE_PLAN':
    return {
      ...state,
      plans: [
      ...state.plans.slice(0, action.idx),
      Object.assign({}, state.plans[action.idx], action.plan),
      ...state.plans.slice(action.idx + 1)
      ]
    };

有没有更快的方法来解决这个问题? 即使我必须使用外部库,或者至少有人可以向我解释如何更好地处理此问题...

谢谢!

docs question

最有用的评论

是的,我们建议对您的数据进行标准化。
这样,您就不必“深入”:所有实体都处于同一级别。

所以你的状态看起来像

{
  entities: {
    plans: {
      1: {title: 'A', exercises: [1, 2, 3]},
      2: {title: 'B', exercises: [5, 1, 2]}
     },
    exercises: {
      1: {title: 'exe1'},
      2: {title: 'exe2'},
      3: {title: 'exe3'}
    }
  },
  currentPlans: [1, 2]
}

您的减速器可能看起来像

import merge from 'lodash/object/merge';

const exercises = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...action.exercise
      }
    };
  case 'UPDATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.exercise
      }
    };
  default:
    if (action.entities && action.entities.exercises) {
      return merge({}, state, action.entities.exercises);
    }
    return state;
  }
}

const plans = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...action.plan
      }
    };
  case 'UPDATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.plan
      }
    };
  default:
    if (action.entities && action.entities.plans) {
      return merge({}, state, action.entities.plans);
    }
    return state;
  }
}

const entities = combineReducers({
  plans,
  exercises
});

const currentPlans = (state = [], action) {
  switch (action.type) {
  case 'CREATE_PLAN':
    return [...state, action.id];
  default:
    return state;
  }
}

const reducer = combineReducers({
  entities,
  currentPlans
});

那么这是怎么回事? 首先,请注意状态已标准化。 我们再也没有其他实体内部的实体。 相反,它们通过ID相互引用。 因此,只要某些对象发生更改,就只有一个地方需要更新。

其次,通知我们如何反应CREATE_PLAN双方将在适当的实体plans减速_and_通过增加其ID添加到currentPlans减速。 这个很重要。 在更复杂的应用程序中,您可能会有关系,例如, plans reducer可以通过向计划内的数组添加新ID来以相同方式处理ADD_EXERCISE_TO_PLAN 。 但是,如果练习本身已更新,则_不需要plans减速器知道这一点,因为ID尚未更改_。

第三,请注意,实体缩减器( plansexercises )具有特殊的条款,当心action.entities 。 如果我们有一个带有“已知事实”的服务器响应,我们想更新所有实体以反映出来。 要在分派操作之前以这种方式准备数据,可以使用normalizr 。 您可以在Redux repo的“真实世界”示例中看到它。

最后,请注意实体化简器是如何相似的。 您可能想编写一个函数来生成这些函数。 这超出了我的答案范围,有时您需要更大的灵活性,有时您需要更少的样板。 您可以在“真实世界”示例化例中查看分页代码,以生成类似的化例。

哦,我用{ ...a, ...b }语法。 它已在Babel第二阶段作为ES7提案启用。 它称为“对象散布算子”,等效于编写Object.assign({}, a, b)

对于库,您可以使用Lodash(请注意不要突变,例如merge({}, a, b}是正确的,但merge(a, b)不是), updeepreact-addons-update或其他方法。 但是,如果您发现自己需要进行深层更新,则可能意味着状态树不够平坦,并且您没有充分利用功能组合。 甚至您的第一个示例:

case 'UPDATE_PLAN':
  return {
    ...state,
    plans: [
      ...state.plans.slice(0, action.idx),
      Object.assign({}, state.plans[action.idx], action.plan),
      ...state.plans.slice(action.idx + 1)
    ]
  };

可以写成

const plan = (state = {}, action) => {
  switch (action.type) {
  case 'UPDATE_PLAN':
    return Object.assign({}, state, action.plan);
  default:
    return state;
  }
}

const plans = (state = [], action) => {
  if (typeof action.idx === 'undefined') {
    return state;
  }
  return [
    ...state.slice(0, action.idx),
    plan(state[action.idx], action),
    ...state.slice(action.idx + 1)
  ];
};

// somewhere
case 'UPDATE_PLAN':
  return {
    ...state,
    plans: plans(state.plans, action)
  };

所有32条评论

建议对嵌套的JSON进行规范化,例如: https :

是的,我们建议对您的数据进行标准化。
这样,您就不必“深入”:所有实体都处于同一级别。

所以你的状态看起来像

{
  entities: {
    plans: {
      1: {title: 'A', exercises: [1, 2, 3]},
      2: {title: 'B', exercises: [5, 1, 2]}
     },
    exercises: {
      1: {title: 'exe1'},
      2: {title: 'exe2'},
      3: {title: 'exe3'}
    }
  },
  currentPlans: [1, 2]
}

您的减速器可能看起来像

import merge from 'lodash/object/merge';

const exercises = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...action.exercise
      }
    };
  case 'UPDATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.exercise
      }
    };
  default:
    if (action.entities && action.entities.exercises) {
      return merge({}, state, action.entities.exercises);
    }
    return state;
  }
}

const plans = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...action.plan
      }
    };
  case 'UPDATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.plan
      }
    };
  default:
    if (action.entities && action.entities.plans) {
      return merge({}, state, action.entities.plans);
    }
    return state;
  }
}

const entities = combineReducers({
  plans,
  exercises
});

const currentPlans = (state = [], action) {
  switch (action.type) {
  case 'CREATE_PLAN':
    return [...state, action.id];
  default:
    return state;
  }
}

const reducer = combineReducers({
  entities,
  currentPlans
});

那么这是怎么回事? 首先,请注意状态已标准化。 我们再也没有其他实体内部的实体。 相反,它们通过ID相互引用。 因此,只要某些对象发生更改,就只有一个地方需要更新。

其次,通知我们如何反应CREATE_PLAN双方将在适当的实体plans减速_and_通过增加其ID添加到currentPlans减速。 这个很重要。 在更复杂的应用程序中,您可能会有关系,例如, plans reducer可以通过向计划内的数组添加新ID来以相同方式处理ADD_EXERCISE_TO_PLAN 。 但是,如果练习本身已更新,则_不需要plans减速器知道这一点,因为ID尚未更改_。

第三,请注意,实体缩减器( plansexercises )具有特殊的条款,当心action.entities 。 如果我们有一个带有“已知事实”的服务器响应,我们想更新所有实体以反映出来。 要在分派操作之前以这种方式准备数据,可以使用normalizr 。 您可以在Redux repo的“真实世界”示例中看到它。

最后,请注意实体化简器是如何相似的。 您可能想编写一个函数来生成这些函数。 这超出了我的答案范围,有时您需要更大的灵活性,有时您需要更少的样板。 您可以在“真实世界”示例化例中查看分页代码,以生成类似的化例。

哦,我用{ ...a, ...b }语法。 它已在Babel第二阶段作为ES7提案启用。 它称为“对象散布算子”,等效于编写Object.assign({}, a, b)

对于库,您可以使用Lodash(请注意不要突变,例如merge({}, a, b}是正确的,但merge(a, b)不是), updeepreact-addons-update或其他方法。 但是,如果您发现自己需要进行深层更新,则可能意味着状态树不够平坦,并且您没有充分利用功能组合。 甚至您的第一个示例:

case 'UPDATE_PLAN':
  return {
    ...state,
    plans: [
      ...state.plans.slice(0, action.idx),
      Object.assign({}, state.plans[action.idx], action.plan),
      ...state.plans.slice(action.idx + 1)
    ]
  };

可以写成

const plan = (state = {}, action) => {
  switch (action.type) {
  case 'UPDATE_PLAN':
    return Object.assign({}, state, action.plan);
  default:
    return state;
  }
}

const plans = (state = [], action) => {
  if (typeof action.idx === 'undefined') {
    return state;
  }
  return [
    ...state.slice(0, action.idx),
    plan(state[action.idx], action),
    ...state.slice(action.idx + 1)
  ];
};

// somewhere
case 'UPDATE_PLAN':
  return {
    ...state,
    plans: plans(state.plans, action)
  };

将其转换为食谱会很好。

理想情况下,我们需要看板板示例。
这对于嵌套实体是完美的,因为“通道”内部可能有“卡片”。

@ andre0799或您可以只使用Immutable.js))

理想情况下,我们需要看板板示例。

我写了一个。 也许您可以分叉并根据自己的喜好进行调整。

Immutable.js并不总是一个好的解决方案。 它会从您更改的节点开始重新计算状态的每个父节点的哈希,在特定情况下这会成为瓶颈(在某些情况下这不是很常见)。 因此,理想情况下,在将Immutable.js集成到您的应用程序之前,应该先进行一些基准测试。

感谢@gaearon的回答,很好的解释!

因此,当您执行CREATE_PLAN您应该自动创建一个默认练习并将其添加到其中。 我应该如何处理这种情况? 然后我应该连续调用3个动作吗? CREATE_PLAN, CREATE_EXERCISE, ADD_EXERCISE_TO_PLAN如果是这样,我应该从哪里拨打这些电话?

因此,当您执行CREATE_PLAN时,您应该自动创建一个默认练习并将其添加到其中。 我应该如何处理这种情况?

通常,我赞成使用许多化简器来处理相同的操作,但对于具有关系的实体而言,它可能变得过于复杂。 实际上,我建议将这些模型建模为单独的动作。

您可以使用Redux Thunk中间件来编写一个将它们都调用的动作创建者:

function createPlan(title) {
  return dispatch => {
    const planId = uuid();
    const exerciseId = uuid();

    dispatch({
      type: 'CREATE_EXERCISE',
      id: exerciseId,
      exercise: {
        id: exerciseId,
        title: 'Default'
      }
    });

    dispatch({
      type: 'CREATE_PLAN',
      id: planId,
      plan: {
        id: planId,
        exercises: [exerciseId],
        title
      }
    });
  };
}

然后,如果您使用Redux Thunk中间件,则可以正常调用它:

store.dispatch(createPlan(title));

假设我在后端的某个位置具有这样的关系(帖子,作者,标签,附件等)的帖子编辑器。

如何显示类似于currentPlans键数组的currentPosts ? 我是否需要将currentPosts每个键映射到mapStateToProps函数中entities.posts中的对应对象? 如何对currentPosts排序?

所有这些都属于还原剂成分吗?

我在这里想念...

关于最初的问题,我相信为此目的而创建了React不变性助手

如何显示类似于currentPlans键数组的currentPosts? 我是否需要将currentPosts中的每个键映射到mapStateToProps函数中entity.posts中的相应对象? 如何对currentPost进行排序?

这是对的。 检索数据时,您将做所有事情。 请查阅Redux repo随附的“购物车”和“现实世界”示例。

谢谢,阅读文档中的“计算衍生数据”后,我已经开始有了一个主意。

我将再次检查这些示例。 当我阅读它们时,我可能一开始并不了解很多事情。

@ andre0799

默认情况下,任何connect() ed组件都将dispatch作为道具注入。

this.props.dispatch(createPlan(title));

这是与该线程无关的使用问题。 最好参考示例或为此创建StackOverflow问题。

我同意Dan规范化数据并尽可能扁平化状态结构的观点。 它可以作为文档中的食谱/最佳实践,因为它可以让我省去一些麻烦。

当我犯了一个关于状态深度的错误时,我创建了这个库来帮助通过Redux改造和管理深度状态: https :
也许我理解错了,但我会对您的反馈意见感兴趣。 希望它会帮助某人。

这很有帮助。 谢谢大家。

您将如何使用与实际示例相同的结构将练习添加到计划中? 假设添加一个练习将返回新创建的练习实体,其中包含planId字段。 是否可以将新的练习添加到该计划中而不必编写计划的精简版,并且专门听CREATE_EXERCISE动作?

很棒的讨论和信息。 我想为我的项目使用normalizr,但是对于将更新的数据保存回远程服务器有疑问。 主要是有没有简单的方法可以将标准化后的形状恢复为更新后远程api提供的嵌套形状? 当客户端进行更改并且需要将其反馈回远程api时,这很重要,在远程api中他无法控制更新请求的形状。

例如:客户端获取嵌套的运动数据->客户端对其进行规范化并将其存储在redux中->用户在客户端上对规范化数据进行更改->用户单击保存->客户端应用将更新后的规范化数据转换回嵌套形式因此它可以将其提交到远程服务器->客户端提交到服务器

如果我使用normalizr,是否需要为加粗的步骤编写一个自定义转换器,或者您是否为此建议使用库或帮助程序方法? 任何建议将不胜感激。

谢谢

有一个叫做https://github.com/gpbl/denormalizr的东西,但是我不确定它跟踪normalizr更新的紧密程度。 我花了几个小时为我开发的应用程序编写了normalizr,欢迎您分叉它并添加反规范化😄。

太酷了,我肯定会研究非规范化,并在我掌握一些东西后为您的项目做出贡献。 辛勤工作了几个小时;-)感谢您回到我身边。

我处于与更改深层嵌套数据相同的情况下,但是,我发现了使用immutable.js的可行解决方案。

如果我链接到我在此处寻求有关解决方案反馈的StackOverflow帖子,可以吗?

我现在正在链接它,请删除我的帖子,或者说是否不合适在这里链接:
http://stackoverflow.com/questions/37171203/manipulating-data-in-nested-arrays-in-redux-with-immutable-js

我见过过去推荐这种方法。 但是,当需要删除嵌套对象时,此方法似乎效果不佳。 在这种情况下,化简器将需要遍历对对象的所有引用并删除它们,这是一个O(n)运算,然后才能删除对象本身。 有人遇到类似问题并解决了吗?

@ariofrio :啊...我很困惑。 规范化的要点是对象不是嵌套存储的,并且对给定项目只有一个引用,因此可以轻松更新该项目。 现在,如果还有多个其他实体通过ID“引用”了该项目,那么肯定也需要更新它们,就像事情没有被标准化一样。

您是否有特定的顾虑,或正在处理的麻烦情况?

这就是我的意思。 说当前状态如下:

{
  entities: {
    plans: {
      1: {title: 'A', exercises: [1, 2, 3]},
      2: {title: 'B', exercises: [5, 6]}
     },
    exercises: {
      1: {title: 'exe1'},
      2: {title: 'exe2'},
      3: {title: 'exe3'}
      5: {title: 'exe5'}
      6: {title: 'exe6'}
    }
  },
  currentPlans: [1, 2]
}

在此示例中,每个练习只能由一个计划引用。 当用户单击“删除运动”时,消息可能看起来像这样:

{type: "REMOVE_EXERCISE", payload: 2}

但是要正确实施,需要遍历所有计划,然后遍历每个计划中的所有练习,以找到引用了ID为2的练习的那个,以避免出现悬而未决的引用。 这是我担心的O(n)操作。

避免这种情况的一种方法是将计划ID包含在REMOVE_EXERCISE的有效载荷中,但是目前,我看不出使用嵌套结构的优势。 如果改为使用嵌套状态,则状态可能类似于:

{
   plans: [
    {title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
    {title: 'B', exercises: [{title: 'exe5'}, {title: 'exe6'}]}
   ]
}

删除该练习的消息可能如下所示:

{type: "REMOVE_EXERCISE", payload: {plan_index: 0, exercise_index: 1}}

一些想法:

  • 您可以对练习进行反向查找,以简化该方案。 类似地,Redux-ORM库所做的是自动为多种类型的关系生成“通过表”。 因此,在这种情况下,您的商店中将有一个“ PlanExercise”“表”,其中将包含{id, planId, exerciseId}三胞胎。 当然是O(n)扫描,但是很简单。
  • O(n)操作本质上不是一件坏事。 这完全取决于N的大小,该术语前面的常量因子,它发生的频率以及应用程序中发生的其他情况。 遍历10或15个项目的列表并在用户按钮单击上进行一些相等性检查,这与例如每500ms遍历系统中的1000万个项目列表并对每个项目执行一些昂贵的操作完全不同。项目。 在这种情况下,很有可能即使检查成千上万的计划也不会成为有意义的瓶颈。
  • 这是您看到的实际性能问题,还是只是展望可能的理论问题?

最终,嵌套状态和规范化状态都是约定。 有充分的理由在Redux中使用归一化状态,并且有充分的理由使您的状态保持归一化。 选择任何适合您的方法:)

我的解决方案是这样的:

function deepCombinReducer(parentReducer, subReducer) {
    return function (state = parentReducer(void(0) /* get parent reducer initial state */, {}) {
        let finalState = {...state};

        for (var k in subReducer) {
          finalState[k] = subReducer(state[k], action);
        }

       return parentReducer(finalState, action);
    };
}

const parentReducer = function(state = {}, action) {
    return state;
}

const subReducer = function(state = [], action) {
    state = Immutable.fromJS(state).toJS();
    switch(action.type) {
       case 'ADD':
          state.push(action.sub);
           return state;
       default:
          return state;
   }
}

export default combineReducers({
   parent: deepCombinReducer(parentReducer, {
       sub: subReducer
   })
})

然后,您可以像这样获得商店:

{
    parent: {
       sub: []
    }
}

dispatch({
    type: 'ADD',
    sub: '123'
});

// the store will change to:
{
    parent: {
       sub: ['123']
    }
}

@smashercosmo immutable.js具有深层嵌套状态? 我很好奇

@gaearon

我们再也没有其他实体内部的实体。

我不明白这里至少有三个嵌套级别:

{
  entities: {
    plans: {
      1: {title: 'A', exercises: [1, 2, 3]},
      2: {title: 'B', exercises: [5, 1, 2]}
     },
    exercises: {
      1: {title: 'exe1'},
      2: {title: 'exe2'},
      3: {title: 'exe3'}
    }
  },
  currentPlans: [1, 2]
}

entities.plans[1] - three levels
entities.exercises[1] - three levels

这是一个非嵌套对象。 只有一级。

{
   plans: [1,2, 3],
   exercises: [1,2,3],
   'so forth': [1,2,3]
}

@wzup :仅供参考,Dan

这里的“嵌套”的意思是当数据本身被嵌套时,就像线程前面的示例一样:

{
   plans: [
    {title: 'A', exercises: [{title: 'exe1'}, {title: 'exe2'},{title: 'exe3'}]},
    {title: 'B', exercises: [{title: 'exe5'}, {title: 'exe6'}]}
   ]
}

在该示例中,访问练习“ exe6”的唯一方法是挖掘结构,例如plans[1].exercises[2]

我对@tmonte的问题感兴趣:

您将如何使用与实际示例相同的结构将练习添加到计划中? 假设添加了一个练习,则返回了新创建的带有planId字段的练习实体。 是否可以将新的练习添加到该计划中,而不必编写计划的精简版,并且专门收听CREATE_EXERCISE操作?

当您有许多实体时,为每个实体创建一个简化器可能很麻烦,但是这种方法可以解决它。 到目前为止,我还没有找到解决方案。

我会预先使用mergeWith而不是merge来获得更大的灵活性:

import mergeWith from 'lodash/mergeWith';

// Updates an entity cache in response to any action with `entities`.
function entities(state = {}, action) {
  // Here where we STORE or UPDATE one or many entities
  // So check if the action contains the format we will manage
  // wich is `payload.entities`
  if (action.payload && action.payload.entities) {
    // if the entity is already in the store replace
    // it with the new one and do not merge. Why?
    // Assuming we have this product in the store:
    //
    // products: {
    //   1: {
    //     id: 1,
    //     name: 'Awesome product name',
    //     rateCategory: 1234
    //   }
    // }
    //
    // We will updated with
    // products: {
    //   1: {
    //     id: 1,
    //     name: 'Awesome product name',
    //   }
    // }
    //
    // The result if we were using `lodash/merge`
    // notice the rate `rateCategory` hasn't changed:
    // products: {
    //   1: {
    //     id: 1,
    //     name: 'Awesome product name',
    //     rateCategory: 1234
    //   }
    // }
    // for this particular use case it's safer to use
    // `lodash/mergeWith` and skip the merge
    return mergeWith({}, state, action.payload.entities, (oldD, newD) => {
      if (oldD && oldD.id && oldD.id === newD.id) {
        return newD;
      }
      return undefined;
    });
  }

  // Here you could register other handlers to manipulate 
  // the entities
  switch (action.type) {
    case ActionTypes.SOME_ACTION:
      // do something;
    default:
      return state;
  }
}

const rootReducer = combineReducers({
  entities,
});
export default rootReducer;
此页面是否有帮助?
0 / 5 - 0 等级

相关问题

ronag picture ronag  ·  46评论

markerikson picture markerikson  ·  51评论

gaearon picture gaearon  ·  69评论

wmertens picture wmertens  ·  55评论

acdlite picture acdlite  ·  54评论