在 Redux 状态中存储数据:API 对象与简化形式状态的反射

Storing data in Redux state: reflection of API object vs. simplified form state

假设我的后端有一个 Report 模型,它有一个 date 属性,类型为 Date。 React 表单包含两个用于修改它的输入:<select> 带有月份,<input> 用于 YYYY 格式的日期年份。我不关心这一天,因为我将使用 moment.utc(...).endOf('month').

将日期设置为所选年份中所选月份的结束

对于这个任务,我看到两个选项:

  1. 我的 Redux 状态包含最终计算的 date 对象,该对象将发布到后端,或者
  2. 状态有两个属性:monthyear 然后,当 Report 准备好发布到后端时,这两个属性被删除并转换为 date对象。

选项 (2) 似乎很脏,需要大量的预处理:将接收到的 Report 对象分解为不属于后端的属性,以及在被接收之前构造所需的属性发布到服务器。

但是,选项 (2) 允许通过组件的 onChangevalue 属性将 React 的组件更清晰地绑定到状态。

选项 (1) 似乎更明智,因为 Redux 状态是后端状态的表示,但是我发现自己写了很多 hacky 逻辑:

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

import { reportChange } from '../actions';

const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];

class Report extends Component {
  constructor(props) {
    super(props);
    this.handleMonthChange = this.handleMonthChange.bind(this);
  }

  handleMonthChange(event) {
    const { dispatch, report } = this.props,
      { target: { value }} = event,
      date = moment.utc(report.date),
      year = this.yearInput.value,
      month = value;
    date.month(month).year(year).endOf('month');
    dispatch(reportChange({
      date: date.toDate()
    }));
  }

  render() {
    const { report } = this.props;

    return (
      <div>
        <select name="month" value={moment.utc(report.date).format('M')} onChange={this.handleMonthChange}>
          {months.map((month, index) => (<option key={index} value={index + 1}>{month}</option>))}
        </select>
        <input
          name="year"
          value={moment.utc(report.date).format('YYYY')} // this sets initial value of the input
          ref={input => this.yearInput = input} // uncontrolled component
        />
      </div>
    );
  }

  static propTypes = {
    dispatch: PropTypes.func.isRequired,
    report: PropTypes.object.isRequired
  }
}

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

export default connect(mapStateToProps)(Report);

这是一个简短的版本,删除了所有不必要的内容。现在,如您所见,我使用 uncontrolled component 作为年份输入。这是因为如果我通过 value={moment.utc(report.date).format('YYYY')}onChange 属性将它绑定到 Redux 存储中 Report 对象中的原始 date 属性 组件变得不可用,它始终绑定到 YYYY 格式,因此您不能删除任何数字 — 当您按退格键从中删除 7 时,它会自动更新为 0201 而不是 201 2017.

此外,由于这是一个不受控制的组件,我必须在所有重要事件中手动提取它的值,例如 formonSubmit 事件处理程序,以便在提交之前更新报告对象合适的日期等

我觉得我错过了什么,应该有更简单的方法。非常感谢任何帮助或建议。

Option (1) seems more sensible, since the Redux state is a representation of the backend's state

我会争论。 Redux 状态应该代表您的应用程序的状态(无论它是什么,包括未存储在后端的 UI 状态)。因此,选项 (2) 是可行的。此外,它使组件可重用,因为它们不依赖于后端数据格式。

我非常不同意组件状态应该紧密映射相关 ReduxFlux,以哪个为准)状态的想法。将视图逻辑与域逻辑混合总是会导致这种复杂情况。

几乎总是A Good Thing在视图代码中使用方便视图逻辑的数据格式,而在域逻辑代码中使用另一种数据格式。并实施必要的桥接代码以将一个代码转换为另一个代码(通过适当的验证等)。您当前使用的 react-redux 为此类转换提供了两个位置:mapStateToPropsmapDispatchToProps

如果您让您的视图代码(React 组件)拥有自己的生命和自己的状态,您将解决您提到的另一种复杂情况,即与不需要的年份字段更新作斗争。

我修复了你的代码,让它看起来像我描述的那样,但是注意我实际上没有运行,所以可能需要一些修复:

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';

import { reportChange } from '../actions';

const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ];

class Report extends Component {
  constructor(props) {
    super(props);
    this.state = { month: props.month, year: props.year }; 
  }

  componentWillReceiveProps(nextProps) {
    this.setState({ month: nextProps.month });
  }

  handleMonthChange(month) {
    this.setState({ month });
    props.onChange(month, this.state.year);
  }

  handleYearChange(year) {
    this.setState({ year });
    props.onChange(this.state.month, year);
  }

  render() {
    return (
      <div>
        <select
          name="month"
          value={this.state.month}
          onChange={e => this.handleMonthChange(e.target.value)}
        >
          {months.map((month, index) => (<option key={index} value={index + 1}>{month}</option>))}
        </select>

        <input
          name="year"
          type="text"
          value={this.state.year}
          onChange={e => handleYearChange(e.target.value)}
        />
      </div>
    );
  }

  static propTypes = {
    onChange: PropTypes.func.isRequired,
    month: PropTypes.string.isRequired,
    year: PropTypes.string.isRequierd,
  }
}

const mapStateToProps = state => {
  const date = state.report.date;
  return {
    month: moment.utc(date).format('M'),
    year: moment.utc(date).format('YYYY'),
  };
});

const mapDispatchToProps = dispatch => ({
  onChange: (month, year) => {
    const date = date.month(month).year(year).endOf('month').toDate();
    dispatch(reportChange({ date })),
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(Report);