在 Redux 存储中构建标准化 JSON 响应并映射到 React 组件道具

Structuring Normalized JSON response in Redux store and mapping to React component props

就在最近,我们的团队开始以规范化的方式构建我们的 JSON 有效负载。我最习惯在 React 组件甚至 reducer 中处理嵌套数据,但我看到了这里的好处(更少连接的组件重新渲染、简化的 reducer 代码和更容易的测试)并且我很高兴开始使用这种方法。然而,在我第一次尝试后,我对状态形状有些困惑。

让我们从有效载荷的形状开始 -

{
      "data": {
        "advisors": {
          "allIds": [
            2
          ], 
          "byId": {
            "2": {
              "active": true, 
              "avatar_url": null, 
              "country": "US", 
              "email": "demo@gmail.com", 
              "first_name": "George Michael", 
              "full_name": "George Michael Bluth", 
              "id": 2, 
              "last_name": "Bluth", 
              "time_zone": "US/Central"
            }
          }
        }, 
        "opportunities": {
          "allIds": [
            "100-3", 
          ], 
          "byId": {
            "100-3": {
              "created": "Fri, 29 Sep 2017 20:00:40 GMT", 
              "program_id": 3, 
              "prospect_id": 100
            }
          }
        }, 
        "programs": {
          "allIds": [ 
            3
          ], 
          "byId": {
            "3": {
              "abbr": "CAP", 
              "end_date": null, 
              "funnel_id": 2, 
              "id": 3, 
              "launch_date": "Sat, 11 Mar 2017 00:00:00 GMT", 
              "name": "Certificate in Astral Projection", 
              "period_end": null, 
              "period_start": null, 
              "program_level_abbr": "NCC", 
              "school_id": 2, 
              "virtual": false
            }
          }
        }, 
        "prospects": {
          "allIds": [
            2,
          ], 
          "byId": {
            "2": {
              "advisor_id": 3, 
              "contact_attempt_count": 0, 
              "contact_success_count": 0, 
              "do_not_call": false, 
              "do_not_email": false, 
              "do_not_mail": false, 
              "email": "adavis.est@hotmail.com", 
              "first_name": "Antonio", 
              "id": 2, 
              "inactive": false, 
              "last_name": "Davis", 
              "phone": {
                "area_code": "800", 
                "extension": "70444", 
                "number": "3575792"
              }, 
              "priority": 10.0, 
              "referred_by_prospect_id": null, 
              "third_party": false
            },
          }
        }
      }, 
      "pagination": {
        "page_number": 1, 
        "total": 251
      }
    }

规范化有效负载的结构使得顾问、机会、计划和潜在客户是兄弟姐妹而不是祖先。它们都嵌套在 "data".

中的一层

然后,在我的 "prospects" reducer 中,我将前景状态初始化为具有以下键的对象:获取、失败和实体。前两个是 UI 数据,实体将包含响应(顾问、机会、计划和潜在客户)。

const initialState = {
  fetching: false,
  failure: false,
  entities: null,
};

function prospects(state = initialState, action) {
  switch (action.type) {
    case constants.prospects.REQUEST_PROSPECTS:
      return { ...state, fetching: true };
    case constants.prospects.RECEIVE_PROSPECTS:
      return Object.assign({}, state, {
        fetching: false,
        entities: action.data,
      });
    case constants.prospects.REQUEST_PROSPECTS_FAILURE:
      return { ...state, fetching: false, failure: true };
    default:
      return state;
  }
}

现在,对于将我带到这里的危险信号 - 我的道具和内部组件状态似乎结构奇怪。我 mapStateToProps 是这样的:

const mapStateToProps = state => ({
  prospects: state.prospects,
});

这让我获得了这样的顾问、机会、计划和潜在客户:

this.props.fetching
this.props.failure
this.props.prospects.entities.advisors.allIds.length
this.props.prospects.entities.opportunities.allIds.length
this.props.prospects.entities.programs.allIds.length
this.props.prospects.entities.prospects.allIds.length

我的理解是,使用规范化方法,事物通常位于 this.props.entities 和 ui 数据下 this.props.ui。问题是我从我的前景行动和减速器而不是单独的行动和减速器中获取所有这些数据吗?我想减少组件中的访问器链,因为它变得非常容易出错且难以阅读。使用单独的 XHR 和 reducer 查询每个实体会更好吗?

我知道有很多关于这种方法的好资源,包括来自 DA 的视频。但我还没有找到所有这些问题的组合答案。谢谢!

总结

我建议您将状态重构为:

{
  network: {
    loading: false,
    failure: false
  },
  advisors: { allIds, byId },
  opportunities: { allIds, byId },
  programs: { allIds, byId },
  prospects: { allIds, byId },
}

为此,您需要为状态中的每个键使用一个 reducer。每个 reducer 将处理其部分规范化有效负载,否则将忽略操作。

减速器

Network.js:

function network(state = { loading: false, failure: false }, action) {
  switch (action.type) {
    case constants.REQUEST_PAYLOAD:
      return { ...state, fetching: true };
    case constants.RECEIVE_PAYLOAD:
      return { ...state, fetching: false, failure: false };
    case constants.prospects.REQUEST_PROSPECTS_FAILURE:
      return { ...state, fetching: false, failure: true };
    default:
      return state;
  }
}

Prospects.js:

function prospects(state = { allIds: [], byId: {} }, action) {
  switch (action.type) {
    case constants.RECEIVE_PAYLOAD:
      // depending on your use case, you may need to merge the existing
      // allIds and byId with the action's. This would allow you to 
      // issue the request multiple times and add to the store instead
      // of overwriting it each time.
      return { ...state, ...action.data.prospects };
    default:
      return state;
  }
}

为状态的每个其他部分重复前景减速器。


备注

我假设您的有效负载从单个 API 调用中以这种方式返回,并且您没有将每个兄弟(顾问、机会、程序和潜在客户)的单独调用拼接在一起).


详情

为了将您的有效负载存储在商店中,我建议编写单独的 reducer,每个 reducer 处理您的 API 调用返回的状态的不同部分。

对于prospects,您应该 存储负载的prospects 部分并丢弃其余部分。

所以不是...

case constants.prospects.RECEIVE_PROSPECTS:
  return Object.assign({}, state, {
    fetching: false,
    entities: action.data,
  });

你应该做...

case constants.prospects.RECEIVE_PROSPECTS:
  return { 
    ...state,
    fetching: false,
    entities: action.data.prospects,
  };

然后为您的 API 调用返回的每种其他类型的数据设置一个类似的缩减器。这些减速器中的每一个都将处理 完全相同的操作 。不过,他们只会处理他们关心的负载部分。

最后,在您的 mapStateToProps 中,state.prospects 将仅包含潜在客户数据。

附带说明——假设我对单个 API 传送的有效载荷是正确的——我会将你的操作常量重命名为 REQUEST_PAYLOADRECEIVE_PAYLOADREQUEST_PAYLOAD_FAILURE,或同样通用的东西。

还有一个建议:您可以将 fetchingfailure 逻辑移动到一个 NetworkReducer 中,它只负责为 [=71= 管理 success/failure/loading ] 要求。这样,您的每个其他减速器只需要处理 RECEIVE 情况并且可以忽略其他操作。