在 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')
.
将日期设置为所选年份中所选月份的结束
对于这个任务,我看到两个选项:
- 我的 Redux 状态包含最终计算的
date
对象,该对象将发布到后端,或者
- 状态有两个属性:
month
和 year
然后,当 Report
准备好发布到后端时,这两个属性被删除并转换为 date
对象。
选项 (2) 似乎很脏,需要大量的预处理:将接收到的 Report
对象分解为不属于后端的属性,以及在被接收之前构造所需的属性发布到服务器。
但是,选项 (2) 允许通过组件的 onChange
和 value
属性将 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
.
此外,由于这是一个不受控制的组件,我必须在所有重要事件中手动提取它的值,例如 form
的 onSubmit
事件处理程序,以便在提交之前更新报告对象合适的日期等
我觉得我错过了什么,应该有更简单的方法。非常感谢任何帮助或建议。
Option (1) seems more sensible, since the Redux state is a representation of the backend's state
我会争论。 Redux 状态应该代表您的应用程序的状态(无论它是什么,包括未存储在后端的 UI 状态)。因此,选项 (2) 是可行的。此外,它使组件可重用,因为它们不依赖于后端数据格式。
我非常不同意组件状态应该紧密映射相关 Redux
(Flux
,以哪个为准)状态的想法。将视图逻辑与域逻辑混合总是会导致这种复杂情况。
几乎总是A Good Thing在视图代码中使用方便视图逻辑的数据格式,而在域逻辑代码中使用另一种数据格式。并实施必要的桥接代码以将一个代码转换为另一个代码(通过适当的验证等)。您当前使用的 react-redux
为此类转换提供了两个位置:mapStateToProps
和 mapDispatchToProps
。
如果您让您的视图代码(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);
假设我的后端有一个 Report
模型,它有一个 date
属性,类型为 Date
。 React 表单包含两个用于修改它的输入:<select>
带有月份,<input>
用于 YYYY
格式的日期年份。我不关心这一天,因为我将使用 moment.utc(...).endOf('month')
.
对于这个任务,我看到两个选项:
- 我的 Redux 状态包含最终计算的
date
对象,该对象将发布到后端,或者 - 状态有两个属性:
month
和year
然后,当Report
准备好发布到后端时,这两个属性被删除并转换为date
对象。
选项 (2) 似乎很脏,需要大量的预处理:将接收到的 Report
对象分解为不属于后端的属性,以及在被接收之前构造所需的属性发布到服务器。
但是,选项 (2) 允许通过组件的 onChange
和 value
属性将 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
.
此外,由于这是一个不受控制的组件,我必须在所有重要事件中手动提取它的值,例如 form
的 onSubmit
事件处理程序,以便在提交之前更新报告对象合适的日期等
我觉得我错过了什么,应该有更简单的方法。非常感谢任何帮助或建议。
Option (1) seems more sensible, since the Redux state is a representation of the backend's state
我会争论。 Redux 状态应该代表您的应用程序的状态(无论它是什么,包括未存储在后端的 UI 状态)。因此,选项 (2) 是可行的。此外,它使组件可重用,因为它们不依赖于后端数据格式。
我非常不同意组件状态应该紧密映射相关 Redux
(Flux
,以哪个为准)状态的想法。将视图逻辑与域逻辑混合总是会导致这种复杂情况。
几乎总是A Good Thing在视图代码中使用方便视图逻辑的数据格式,而在域逻辑代码中使用另一种数据格式。并实施必要的桥接代码以将一个代码转换为另一个代码(通过适当的验证等)。您当前使用的 react-redux
为此类转换提供了两个位置:mapStateToProps
和 mapDispatchToProps
。
如果您让您的视图代码(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);