React JS this.props.data 未在 child 组件的 getInitialState 中定义,即使它已在渲染中定义(并在 parent 中定义)
React JS this.props.data not defined in getInitialState for child component even though it is defined in render (and is defined in parent)
场景:
- Re-using 组件 (
<Row />
-> <Cell />
) 在同一个 grandparent 包装器 (<ShippingTable />
-> <Grid />
) 中代码 re-use。
- 首先,我分配了一个数据数组并循环到 re-use child 组件(
<Row />
-> <Cell />
)。
- 对于第二个,它只是一个 object(与数组的 objects 具有相同的属性),我直接在渲染中分配(不需要
this.props.data.map
循环因为已经只有一个 object).
问题:
- 对于数组,一切按要求进行。
this.props.data
传给 children,通过各种事件更新状态,一切正常。
- 但是,对于单个 object,在
<Row />
之前一切正常。尽管 this.props.data 包含有效值并正确分配给 child <Cell />
组件,但在 <Cell />
的 getInitialState 中,它莫名其妙地未定义(或设置为任何初始值设置在 <ShippingTable />
的 getInitialState
中)。尽管,除了在 .map 循环中被调用之外,它 完全相同的代码和数据在我的数据数组的相同渲染中工作 .
- 此外,
<Cell />
的渲染中的 this.props.data
确实存在且准确,但由于 getInitialState 中的 setState 失败,this.state.data 未定义(或设置为任何设置的值在 <ShippingTable />
的 getInitialState
).
- Double 此外,如果我强制执行 UI re-render(例如,在 grandparent
<ShippingTable />
中更改另一个状态 属性),一切正常我一开始是怎么期待的。
就好像 <Cell />
中的 getInitialState
只有我的单个百分比 object 在成功 AJAX population of setState 之前被调用,或者它没有被再次调用在使用来自服务器的更新数据填充后状态发生更改时。
这是实际代码的精简版(但仍然很长):
更新 - 已解决
永远不要忘记使用 React,始终向下发送道具,向上发送事件,并始终将状态保持在尽可能高的位置。通过将 <Cell />
和 <Row />
的 state-tracking 职责移回到实际跟踪状态的 ShippingTable(而不是引用由 parent 传递下来的道具)来删除它们(并且应该始终被跟踪)。根据下面的@rallrall,我偏离了轨道,并且莫名其妙地与框架作对。回想起来一切都非常清楚(尽管为什么不正确的方法适用于数组而不是 object 确实混淆了水域 - 特别是当将 object 切换到数组时最终也有效)。
var ShippingInput = React.createClass({
getInitialState: function() {
return { value: this.props.value };
},
handleChange: function(event) {
var value = event.target.value;
...conversion to decimal and validation
this.props.onChange(value);
this.setState({ value: value });
},
render: function() {
return (
<Input type="text" placeholder="0.00" bsStyle={this.validationState()} addonBefore={addOnBefore}
value={this.state.value} label={this.props.label} ref="input" onChange={this.handleChange}
groupClassName={this.props.groupClassName} labelClassName={this.props.labelClassName} onKeyDown={this.props.onKeyDown} />
);
}
});
var Cell = React.createClass({
propTypes: {
data: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
onRowEdit: React.PropTypes.func.isRequired
},
getInitialState: function() {
// At this point, this.props.data is undefined *only for <Cell data={this.props.percentages} /> in <Row /> even though that prop is not null there.
return {value: this.props.data};
},
handleChange: function(value) {
this.props.onChange(value);
this.setState({ value: value });
},
render: function() {
var edit = this.props.edit;
var showInput = edit ? 'group-class' : 'group-class hide';
var showText = edit ? 'hide' : 'btn btn-default btn-sm';
var val = this.props.isRates ? accounting.formatMoney(this.state.value) : this.state.value;
// {this.state.value} is undefined here for only the percentages object
// {this.props.data} is *not undefined*
var input = <ShippingInput type="text" label={this.props.label} value={this.state.value} ref="input"
isRates={this.props.isRates} groupClassName={showInput} labelClassName="label-class sr-only" onKeyDown={this.handleKeyDown} onChange={this.handleChange} />;
var text = (<a href="#" className={showText} onClick={this.handleClick}>{val}</a>);
return ( <td>{input}{text}</td> );
}
});
var Row = React.createClass({
propTypes: {
data: React.PropTypes.object.isRequired,
onCellChange: React.PropTypes.func.isRequired,
onRowCommit: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {edit: false};
},
handleChange: function(prop, val) {
this.props.onCellChange(prop, val);
},
...
render: function() {
var edit = this.state.edit;
var text = edit ? 'fa fa-save fa-fw' : 'fa fa-edit fa-fw';
return <tr>
<Cell data={this.props.data.Canada} isRates={this.props.isRates} label="Canada" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "Canada")} />
<Cell data={this.props.data.Us} isRates={this.props.isRates} label="United States" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "Us")} />
<Cell data={this.props.data.International} isRates={this.props.isRates} label="International" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "International")} />
<td>
<Button href="#" ref="commit" onClick={this.handleRowCommit} bsStyle="primary" bsSize="small"><span className={text}></span></Button>
</td>
</tr>;
}
});
var Grid = React.createClass({
propTypes: {
data: React.PropTypes.array.isRequired,
percentages: React.PropTypes.object.isRequired,
onCellChange: React.PropTypes.func.isRequired,
onRowCommit: React.PropTypes.func.isRequired
},
render: function() {
var rows = this.props.data.map(function(rowData, index) {
var id = rowData["Id"];
return <Row key={id} isRates={true} data={rowData} onCellChange={this.props.onCellChange.bind(null, index)} onRowCommit={this.props.onRowCommit.bind(null, index)} onRowDelete={this.props.onRowDelete.bind(null, index)} />;
}, this);
return (
<Table striped bordered hover responsive>
<thead>
<tr>
<th className="col-sm-4">Order Subtotal (up to)</th>
<th className="col-sm-2">Canada</th>
<th className="col-sm-2">US</th>
<th className="col-sm-2">International</th>
<th className="col-sm-1"></th>
</tr>
</thead>
<tbody>
{rows}
<tr><td colSpan="5">If the order subtotal is greater than the largest amount on the above chart, the following rates apply:</td></tr>
<Row key="Percentages" isRates={false} data={this.props.percentages} onCellChange={this.props.onPercentCellChange} onRowCommit={this.props.onPercentRowCommit} />
</tbody>
</Table>
);
}
});
var ShippingTable = React.createClass({
getInitialState: function() {
return this.props.initialData;
},
loadFromServer: function() {
$.getJSON(this.props.url, function(data) {
if (!data || data.Success === false) {
toastr.error('Error loading shipping costs. Please refresh the page and try again.');
} else if (this.isMounted()) {
// This change is not reflected in Row/Cell for this.state/props.percentages until after force a UI update (via handleAdd and handleCancel
// where this.state.add changes) even though a) percentages (a single object) holds valid values here and does all the way down to <Row />
// and b) there is no similar issue with this.state.data.
this.setState({ data: data.Value.ShippingCostMatrix, percentages: data.Value.ShippingPercentage });
}
}.bind(this));
},
componentDidMount: function() {
this.loadFromServer();
},
handleCellChange: function(rowIdx, prop, val) {
var row = copy(this.state.data[rowIdx]);
row[prop] = val;
var rows = this.state.data.slice();
rows[rowIdx] = row;
rows.sort(sortBySubtotal);
this.setState({data: rows});
},
handlePercentCellChange: function(prop, val) {
var row = copy(this.state.percentages);
row[prop] = val;
this.setState({percentages: row});
},
handleAdd: function(event) {
event.preventDefault();
this.setState({ add: true});
},
handleAddCancel: function(event) {
event.preventDefault();
this.setState({ add: false});
},
render: function() {
var ctrl;
if (this.state.add) {
ctrl = (<NewRow onAddCancel={this.handleAddCancel} onRowAdd={this.handleRowAdd} />);
}
else {
ctrl = (
<div>
<p><a onClick={this.handleAdd} className="btn btn-primary btn-lg">Add</a></p>
<Grid data={this.state.data} percentages={this.state.percentages}
onCellChange={this.handleCellChange} onPercentCellChange={this.handlePercentCellChange} onRowCommit={this.handleRowCommit} onPercentRowCommit={this.handlePercentRowCommit} onRowDelete={this.handleRowDelete} />
</div>
);
}
return <div>{ctrl}</div>;
}
});
//React.render(<ShippingTable initialData={ {data : [], percentages: { Canada: 1, Us: 1.25, International: 2.25, Id: 1 }, add: false} }
React.render(<ShippingTable initialData={ {data : [], percentages : {}, add: false} }
url="/admin/shipping/costs" update="/admin/shipping/update" create="/admin/shipping/create" delete="/admin/shipping/delete" updatePercentage="/admin/shipping/updatepercentage" />, document.getElementById('shipTable'));
getInitialState 应该 return 初始组件状态,而不管 props。如果你真的想在状态中设置一个 prop 值,你应该使用 componentWillMount 钩子。参见 docs。
虽然你在这里似乎是在违背框架。当你想改变一个 prop 值时,父组件应该对此做出反应,并为子组件提供新的 props。
场景:
- Re-using 组件 (
<Row />
-><Cell />
) 在同一个 grandparent 包装器 (<ShippingTable />
-><Grid />
) 中代码 re-use。 - 首先,我分配了一个数据数组并循环到 re-use child 组件(
<Row />
-><Cell />
)。 - 对于第二个,它只是一个 object(与数组的 objects 具有相同的属性),我直接在渲染中分配(不需要
this.props.data.map
循环因为已经只有一个 object).
问题:
- 对于数组,一切按要求进行。
this.props.data
传给 children,通过各种事件更新状态,一切正常。 - 但是,对于单个 object,在
<Row />
之前一切正常。尽管 this.props.data 包含有效值并正确分配给 child<Cell />
组件,但在<Cell />
的 getInitialState 中,它莫名其妙地未定义(或设置为任何初始值设置在<ShippingTable />
的getInitialState
中)。尽管,除了在 .map 循环中被调用之外,它 完全相同的代码和数据在我的数据数组的相同渲染中工作 . - 此外,
<Cell />
的渲染中的this.props.data
确实存在且准确,但由于 getInitialState 中的 setState 失败,this.state.data 未定义(或设置为任何设置的值在<ShippingTable />
的getInitialState
). - Double 此外,如果我强制执行 UI re-render(例如,在 grandparent
<ShippingTable />
中更改另一个状态 属性),一切正常我一开始是怎么期待的。
就好像 <Cell />
中的 getInitialState
只有我的单个百分比 object 在成功 AJAX population of setState 之前被调用,或者它没有被再次调用在使用来自服务器的更新数据填充后状态发生更改时。
这是实际代码的精简版(但仍然很长):
更新 - 已解决
永远不要忘记使用 React,始终向下发送道具,向上发送事件,并始终将状态保持在尽可能高的位置。通过将 <Cell />
和 <Row />
的 state-tracking 职责移回到实际跟踪状态的 ShippingTable(而不是引用由 parent 传递下来的道具)来删除它们(并且应该始终被跟踪)。根据下面的@rallrall,我偏离了轨道,并且莫名其妙地与框架作对。回想起来一切都非常清楚(尽管为什么不正确的方法适用于数组而不是 object 确实混淆了水域 - 特别是当将 object 切换到数组时最终也有效)。
var ShippingInput = React.createClass({
getInitialState: function() {
return { value: this.props.value };
},
handleChange: function(event) {
var value = event.target.value;
...conversion to decimal and validation
this.props.onChange(value);
this.setState({ value: value });
},
render: function() {
return (
<Input type="text" placeholder="0.00" bsStyle={this.validationState()} addonBefore={addOnBefore}
value={this.state.value} label={this.props.label} ref="input" onChange={this.handleChange}
groupClassName={this.props.groupClassName} labelClassName={this.props.labelClassName} onKeyDown={this.props.onKeyDown} />
);
}
});
var Cell = React.createClass({
propTypes: {
data: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
onRowEdit: React.PropTypes.func.isRequired
},
getInitialState: function() {
// At this point, this.props.data is undefined *only for <Cell data={this.props.percentages} /> in <Row /> even though that prop is not null there.
return {value: this.props.data};
},
handleChange: function(value) {
this.props.onChange(value);
this.setState({ value: value });
},
render: function() {
var edit = this.props.edit;
var showInput = edit ? 'group-class' : 'group-class hide';
var showText = edit ? 'hide' : 'btn btn-default btn-sm';
var val = this.props.isRates ? accounting.formatMoney(this.state.value) : this.state.value;
// {this.state.value} is undefined here for only the percentages object
// {this.props.data} is *not undefined*
var input = <ShippingInput type="text" label={this.props.label} value={this.state.value} ref="input"
isRates={this.props.isRates} groupClassName={showInput} labelClassName="label-class sr-only" onKeyDown={this.handleKeyDown} onChange={this.handleChange} />;
var text = (<a href="#" className={showText} onClick={this.handleClick}>{val}</a>);
return ( <td>{input}{text}</td> );
}
});
var Row = React.createClass({
propTypes: {
data: React.PropTypes.object.isRequired,
onCellChange: React.PropTypes.func.isRequired,
onRowCommit: React.PropTypes.func.isRequired
},
getInitialState: function() {
return {edit: false};
},
handleChange: function(prop, val) {
this.props.onCellChange(prop, val);
},
...
render: function() {
var edit = this.state.edit;
var text = edit ? 'fa fa-save fa-fw' : 'fa fa-edit fa-fw';
return <tr>
<Cell data={this.props.data.Canada} isRates={this.props.isRates} label="Canada" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "Canada")} />
<Cell data={this.props.data.Us} isRates={this.props.isRates} label="United States" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "Us")} />
<Cell data={this.props.data.International} isRates={this.props.isRates} label="International" edit={edit} onRowEdit={this.handleRowEdit} onRowCommit={this.handleRowCommit} onChange={this.handleChange.bind(null, "International")} />
<td>
<Button href="#" ref="commit" onClick={this.handleRowCommit} bsStyle="primary" bsSize="small"><span className={text}></span></Button>
</td>
</tr>;
}
});
var Grid = React.createClass({
propTypes: {
data: React.PropTypes.array.isRequired,
percentages: React.PropTypes.object.isRequired,
onCellChange: React.PropTypes.func.isRequired,
onRowCommit: React.PropTypes.func.isRequired
},
render: function() {
var rows = this.props.data.map(function(rowData, index) {
var id = rowData["Id"];
return <Row key={id} isRates={true} data={rowData} onCellChange={this.props.onCellChange.bind(null, index)} onRowCommit={this.props.onRowCommit.bind(null, index)} onRowDelete={this.props.onRowDelete.bind(null, index)} />;
}, this);
return (
<Table striped bordered hover responsive>
<thead>
<tr>
<th className="col-sm-4">Order Subtotal (up to)</th>
<th className="col-sm-2">Canada</th>
<th className="col-sm-2">US</th>
<th className="col-sm-2">International</th>
<th className="col-sm-1"></th>
</tr>
</thead>
<tbody>
{rows}
<tr><td colSpan="5">If the order subtotal is greater than the largest amount on the above chart, the following rates apply:</td></tr>
<Row key="Percentages" isRates={false} data={this.props.percentages} onCellChange={this.props.onPercentCellChange} onRowCommit={this.props.onPercentRowCommit} />
</tbody>
</Table>
);
}
});
var ShippingTable = React.createClass({
getInitialState: function() {
return this.props.initialData;
},
loadFromServer: function() {
$.getJSON(this.props.url, function(data) {
if (!data || data.Success === false) {
toastr.error('Error loading shipping costs. Please refresh the page and try again.');
} else if (this.isMounted()) {
// This change is not reflected in Row/Cell for this.state/props.percentages until after force a UI update (via handleAdd and handleCancel
// where this.state.add changes) even though a) percentages (a single object) holds valid values here and does all the way down to <Row />
// and b) there is no similar issue with this.state.data.
this.setState({ data: data.Value.ShippingCostMatrix, percentages: data.Value.ShippingPercentage });
}
}.bind(this));
},
componentDidMount: function() {
this.loadFromServer();
},
handleCellChange: function(rowIdx, prop, val) {
var row = copy(this.state.data[rowIdx]);
row[prop] = val;
var rows = this.state.data.slice();
rows[rowIdx] = row;
rows.sort(sortBySubtotal);
this.setState({data: rows});
},
handlePercentCellChange: function(prop, val) {
var row = copy(this.state.percentages);
row[prop] = val;
this.setState({percentages: row});
},
handleAdd: function(event) {
event.preventDefault();
this.setState({ add: true});
},
handleAddCancel: function(event) {
event.preventDefault();
this.setState({ add: false});
},
render: function() {
var ctrl;
if (this.state.add) {
ctrl = (<NewRow onAddCancel={this.handleAddCancel} onRowAdd={this.handleRowAdd} />);
}
else {
ctrl = (
<div>
<p><a onClick={this.handleAdd} className="btn btn-primary btn-lg">Add</a></p>
<Grid data={this.state.data} percentages={this.state.percentages}
onCellChange={this.handleCellChange} onPercentCellChange={this.handlePercentCellChange} onRowCommit={this.handleRowCommit} onPercentRowCommit={this.handlePercentRowCommit} onRowDelete={this.handleRowDelete} />
</div>
);
}
return <div>{ctrl}</div>;
}
});
//React.render(<ShippingTable initialData={ {data : [], percentages: { Canada: 1, Us: 1.25, International: 2.25, Id: 1 }, add: false} }
React.render(<ShippingTable initialData={ {data : [], percentages : {}, add: false} }
url="/admin/shipping/costs" update="/admin/shipping/update" create="/admin/shipping/create" delete="/admin/shipping/delete" updatePercentage="/admin/shipping/updatepercentage" />, document.getElementById('shipTable'));
getInitialState 应该 return 初始组件状态,而不管 props。如果你真的想在状态中设置一个 prop 值,你应该使用 componentWillMount 钩子。参见 docs。
虽然你在这里似乎是在违背框架。当你想改变一个 prop 值时,父组件应该对此做出反应,并为子组件提供新的 props。