43

查看README中的示例:

鉴于“坏”结构:

[{
  id: 1,
  title: 'Some Article',
  author: {
    id: 1,
    name: 'Dan'
  }
}, {
  id: 2,
  title: 'Other Article',
  author: {
    id: 1,
    name: 'Dan'
  }
}]

添加新对象非常容易。我所要做的就是像

return {
  ...state,
  myNewObject
}

在减速机中。

现在考虑到“好”树的结构,我不知道应该如何处理它。

{
  result: [1, 2],
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 1
      }
    },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      }
    }
  }
}

我想到的每一种方法都需要一些复杂的对象操作,这让我觉得我没有走在正确的轨道上,因为 normalizr 应该让我的生活更轻松。

我在网上找不到任何以这种方式使用 normalizr 树的人的示例。官方示例没有添加和删除,所以它也没有帮助。

有人可以让我知道如何以正确的方式从 normalizr 树中添加/删除吗?

4

5 回答 5

35

以下内容直接来自 redux/normalizr 创建者的帖子

所以你的状态看起来像:

{
  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 相互引用。因此,每当某些对象发生变化时,只有一个地方需要更新。

其次,请注意我们如何通过在计划减速器中添加适当的实体并将其 ID 添加到 currentPlans 减速器来对 CREATE_PLAN 做出反应。这个很重要。在更复杂的应用程序中,您可能会有关系,例如,计划缩减程序可以通过将新 ID 附加到计划内的数组来以相同的方式处理 ADD_EXERCISE_TO_PLAN。但是如果练习本身更新了,则计划 reducer 不需要知道这一点,因为 ID 没有改变。

第三,注意实体reducers(计划和练习)有特殊的条款注意action.entities。这是为了以防我们有一个带有“已知事实”的服务器响应,我们想要更新我们所有的实体来反映。要在分派操作之前以这种方式准备数据,您可以使用 normalizr。你可以在 Redux repo 的“真实世界”示例中看到它的使用情况。

最后,注意实体化简器的相似之处。您可能想编写一个函数来生成这些。这超出了我的回答范围——有时你想要更多的灵活性,有时你想要更少的样板。您可以查看“真实世界”示例减速器中的分页代码,以获取生成类似减速器的示例。

哦,我使用了 { ...a, ...b } 语法。它在 Babel 阶段 2 中作为 ES7 提案启用。它被称为“对象扩展运算符”,相当于编写 Object.assign({}, a, b)。

至于库,您可以使用 Lodash(注意不要发生变异,例如 merge({}, a, b} 是正确的,但 merge(a, b) 不是)、updeep、react-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)
  };
于 2016-01-22T21:01:13.160 回答
4

大多数时候,我对从 API 获得的数据使用 normalizr,因为我无法控制(通常)深层嵌套数据结构。让我们区分实体和结果及其用法。

实体

所有纯数据都在规范化后的实体对象中(在您的情况下articlesusers。我建议要么对所有实体使用 reducer,要么对每个实体类型使用 reducer。实体化简器应负责使您的(服务器)数据保持同步并拥有单一的事实来源。

const initialState = {
  articleEntities: {},
  userEntities: {},
};

结果

结果只是对您的实体的引用。想象以下场景:(1)您从推荐的 APIarticles中获取ids: ['1', '2']. 您将实体保存在您的文章实体 reducer中。(2) 现在,您使用 获取特定作者撰写的所有文章id: 'X'。再次同步文章实体 reducer中的文章。文章实体化简器是所有文章数据的唯一真实来源——就是这样。现在您想要有另一个地方来区分文章((1)推荐文章和(2)作者 X 的文章)。您可以轻松地将它们保存在另一个特定于用例的 reducer 中。该 reducer 的状态可能如下所示:

const state = {
  recommended: ['1', '2' ],
  articlesByAuthor: {
    X: ['2'],
  },
};

现在您可以很容易地看到作者 X 的文章也是推荐文章。但是你在你的文章实体化简器中只保留了一个单一的事实来源。

在您的组件中,您可以简单地映射实体 + 推荐的 /articlesByAuthor 来呈现实体。

免责声明:我可以推荐一篇我写的博客文章,它展示了一个真实世界的应用程序如何使用 normalizr 来防止状态管理中的问题:Redux Normalizr:改进你的状态管理

于 2016-07-29T20:04:31.247 回答
2

我已经实现了一个可以在互联网上找到的通用减速器的小偏差。它能够从缓存中删除项目。您所要做的就是确保在每次删除时发送一个带有已删除字段的操作:

export default (state = entities, action) => {
    if (action.response && action.response.entities)
        state = merge(state, action.response.entities)

    if (action.deleted) {
        state = {...state}

        Object.keys(action.deleted).forEach(entity => {
            let deleted = action.deleted[entity]

            state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key))
                .reduce((p, id) => ({...p, [id]: state[entity][id]}), {})
        })
    }

    return state
}

动作代码中的使用示例:

await AlarmApi.remove(alarmId)

dispatch({
    type: 'ALARM_DELETED',
    alarmId,
    deleted: {alarms: [alarmId]},
})
于 2016-12-14T14:13:02.880 回答
1

派对迟到了几年,但这里——

您可以使用normalized-reducer轻松管理规范化减速器状态而无需样板。你传入一个描述关系的模式,它会返回reducer、actions和selector来管理那个状态片。

import makeNormalizedSlice from 'normalized-reducer';

const schema = {
  user: {
    articles: {
      type: 'article', cardinality: 'many', reciprocal: 'author'
    }
  },
  article: {
    author: {
      type: 'user', cardinality: 'one', reciprocal: 'articles'
    }
  }
};

const {
  actionCreators,
  selectors,
  reducer,
  actionTypes,
  emptyState
} = makeNormalizedSlice(schema);

这些操作允许您执行基本的 CRUD 逻辑以及更复杂的逻辑,例如关系附件/分离、级联删除和批处理操作。

继续这个例子,状态看起来像:

{
  "entities": {
    "user": {
      "1": { 
        "id": "1", 
        "name": "Dan",
        "articles": ["1", "2"]
      }
    },
    "article": {
      "1": { 
        "id": "1",
        "author": "1",
        "title": "Some Article",
      },
      "2": {
        "id": "2",
        "author": "1",
        "title": "Other Article",
      }
    }
  },
  "ids": {
    "user": ["1"],
    "article": ["1", "2"]
  }
}

Normalized Reducer 还与 normalizr 集成:

import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'

const denormalizedData = {...}
const normalizrSchema = {...}

const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);

normalizr 集成的另一个例子

于 2020-04-18T20:22:16.207 回答
0

在你的 reducer 中,保留一份非标准化数据的副本。这样,您可以执行以下操作(将新对象添加到状态数组时):

case ACTION:
  return {
    unNormalizedData: [...state.unNormalizedData, action.data],
    normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
  }

如果您不想在存储中保留未规范化的数据,也可以使用denormalize

于 2017-08-29T20:12:46.207 回答