Redux:如何跟踪数据是否在本地修改?

时间:2016-08-04 19:01:57

标签: javascript reactjs redux state

我知道这是一个意见问题,而且很长,但我在Redux中找到一个好的解决方案时遇到了麻烦。

我正在构建一个关卡编辑器,我想向用户显示数据是否已被修改,因为它已持久保存到服务器。首先,考虑一下数据:

chapters: [{
    id: 1,
    levelIds: [ 2 ]
}],
levels: [{
     id: 2,
     entityIds: [ 4, 5 ]
}],
entities: [{
     id: 4, position:...
}, {
     id: 5, position:...
}]

章节有多个级别,级别有多个实体。在关卡编辑器中,您可以将完整章节编辑为一个项目,因此,如果任何实体或关卡更改,则整个章节将被视为未保存。

我想跟踪用户是否对数据进行了任何更改,因为它上次持久保存到服务器。我想在章节名称旁边显示*如果事情发生了变化标准:

  • 跟踪未保存(未保留到服务器)状态
  • 状态必须与撤消/重做系统一起使用
  • 如果某些"嵌套"数据被更改(如实体位置),顶级章节必须知道它未保存,而不是实体本身

我已经探讨了一些选项,并且我将尝试说明为什么我不确定任何解决方案是否比其他解决方案更好。

选项1:存储"未保存的"每章的标志

此解决方案涉及存储"未保存的" flag,可能在单独的reducer中,在任何修改时设置为true,并在章节保存到服务器时设置为false

问题

  1. 我需要跟踪很多动作,所以这有点冗长。我还需要手动跟踪哪些动作实际修改了章节。它可能看起来像:
  2. function isUnsavedReducer( state = {}, action ):Object {
        switch( action.type ) {
            case CHANGE_ENTITY_POSITION:
            case CHANGE_ENTITY_NAME
            ...etc
            case CHANGE_LEVEL_TITLE: {
                return {
                    ...state,
                    [ action.chapterId ]: true
                };
            }
        }
    }
    
    1. 大部分操作都不了解chapterId。例如,如果我移动实体,则操作看起来像{ entityId: 2, position: newPosition }。如果我走这条路线,我想我必须将chapterId添加到所有操作中,即使他们没有修改章节吗?
    2. 选项2:跟踪最后一章保存的对象

      从表面上看,这看起来更简单。只要数据保持不变,只需存储当前内存中的章节对象:

      function lastSavedReducer( state = {}, action ):Object {
          switch( action.type ) {
              case SAVE_CHAPTER: {
                  return {
                      ...state,
                      [ action.chapterId ]: action.chapter
                  };
              }
          }
      }
      

      然后在视图中检查当前数据是否未保存,它是一个严格的相等检查:

      { lastSaved[ currentChapterId ] === this.props.chater ? 'Saved' : 'Unsaved' }
      

      问题:

      1. 与上面的问题#2相同。当我使用redux动作修改实体位置时,我不会修改顶级章节本身。我必须修改我的所有Reducer,如chapterReducer,以返回一个新对象(即使实际上没有任何改变)。我还可以存储"最后一个持久化的实体"对象,但由于所有实体都存放在一个商店中,我无法跟踪未保存的章节,只是未保存的内容。
      2. 我是否有一个明显的解决方案?我应该修改我的数据存储方式还是我的减速器设置?我的Reducer中的规范化数据,以及可以设置" unsaved"的许多可能操作,使我不确定最佳前进方式。你有没有实现类似的东西并且已经知道了陷阱或最佳前进方向?

1 个答案:

答案 0 :(得分:0)

问题是您已将数据标准化,并且此数据充当您的应用程序的真实来源。

为什么您希望尚未保存的版本事件直接修改此规范化数据?

  • 如果不从后端重新获取保存状态,很难恢复到保存状态(不应该要求)
  • 令人困惑,因为我们不知道是否保存了规范化数据(您当前的问题)

最佳解决方案:在列表中保留未保存的操作,但在保存

之前不要运行它们

您可以在商店列表中累积版本操作,而不是直接修改规范化数据。

在渲染时,您可以使用此列表投影Redux存储状态,因此存储状态不会更改,但您仍可以连接到当前版本状态的组件。这种技术与事件源和快照非常相似。

想象一下你的商店状态如下:

const storeState = {
    unsavedEditionActions: [a1,a2,a3],
    normalizedData: {
      chapters: [{
        id: 1,
        levelIds: [ 2 ]
      }],
      levels: [{
         id: 2,
         entityIds: [ 4, 5 ]
      }],
      entities: [{
         id: 4, position:...
      }, {
         id: 5, position:...
      }]
    }
}

如果某个组件需要获得版本状态和原始状态,您可以直接将其缩减为mapStateToProps

let Component = ...

Component = connect(state => {

  return {
    normalizedData: state.normalizedData,
    normalizedDataEdited: state.unsavedEditionActions.reduce(myNormalizedDataReducer,state.normalizedData)
  }

})(Component)

您的组件现在将接收当前保存的规范化数据和当前草稿版数据。从那时起,您就可以完成您的魔术和浅层比较2个DB的所有内容,以了解已编辑的内容。

在保存时,您清空列表并将其应用于规范化数据。 取消时,您只需清空此列表(无需重新删除,因为您在版本保持完整之前保持状态)。

我建议使用Reselect和ImmutableJS来获得更好的表现,但没有它也应该可以正常工作。

请注意,这种维护事件列表的技术也是许多人用于乐观更新的技术(即:在用户操作后立即提供反馈,无需等待服务器批准,但能够恢复到以前的状态如果服务器不批准更改)。一些有用的链接:

请注意,您不必在connect中减少动作列表,您也可以直接在商店中保留2个条目:normalizedData / normalizedDataEdited。

其他解决方案:构建图表以从标准化数据进行编辑,并比较initialGraph!= editedGraph

您可以构建临时非规范化图,而不是直接修改版本上的规范化数据。因此,您可以构建此图表的副本,编辑副本,并将副本与原始图表进行浅层比较,以查看它是否已被修改。

function levelEditionReducer( state = {}, action ) {
    switch( action.type ) {
        case LEVEL_DATA_LOADED:
          const levelData = action.payload.levelData;
          return {
            initialGraph: levelData,
            editedGraph: levelData,
          }  
        case CHANGE_ENTITY_NAME
          const newEditedGraph = someFunctionToEditEntityName(state.editedGraph,action);
          return (newEditedGraph == state.editedGraph) ? state : { ...state, editedGraph: newEditionState }
        ......
    }
}

然后在版本之后,您可以看到state.initialGraph != state.editedGraph并显示保存按钮和草稿状态。

请注意,只有在非常谨慎地处理不可变数据结构时才会使用浅层比较,并确保不更新任何不必要的内容。如果someFunctionToEditEntityName返回与输入相同的状态,则reducer返回完全相同的状态非常重要,因为该操作实际上没有改变任何东西!

使用ES6编写这种代码并不简单,但如果你使用其他库如Immutable或updeep或类似的东西,如果你试图将一个属性设置为一个已经拥有的值的属性,它可能会短路操作并确保返回原始对象。

另请注意,您会尽早发现缺少护理,因为“保存”按钮会显示,但不应该显示:)所以您可能不会错过。

最后保存数据后,您可以将保存的图表合并到标准化数据