Scenario:
<Row />
-> <Cell />
) in the same grandparent wrapper (<ShippingTable />
-> <Grid />
) for code re-use. <Row />
-> <Cell />
).this.props.data.map
loop as already just one object).Problem:
this.props.data
is passed down to children, state is updated through various events and all is well.<Row />
. Even though this.props.data contains valid values and is properly assigned to the child <Cell />
component, in <Cell />
's getInitialState, it is inexplicably undefined (or set to whatever initial values set in <ShippingTable />
's getInitialState
). even though, apart from being called in a .map loop, it is exactly the same code and data that works in the same rendering for my array of data.this.props.data
in <Cell />
's render is indeed present and accurate, but because of the failed setState in getInitialState, this.state.data is undefined (or set to whatever values set in <ShippingTable />
's getInitialState
).<ShippingTable />
), everything works as how I would expect it in the first place.It's as if getInitialState
in <Cell />
for only my single percentages object is being called before a successful AJAX population of setState, or it's not being called again when the state is changed after being populated with updated data from the server.
Here's a stripped down (but still long) version of the actual code:
UPDATE - Solved
Never forget that with React, always send props down, send events up, and always keep state as far up to the top as possible. Removed <Cell />
and <Row />
's state-tracking responsibilities by moving them back up to the ShippingTable (instead referencing props passed down by parent) where state is actually tracked (and should always be tracked). As per @rallrall below, I got off track and was inexplicably working against the framework. All very clear in retrospect (although why the incorrect approach worked for the array but not the object did muddy the waters - especially when switching the object to an array ended up working as well).
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 is supposed to return the initial component state, regardless of the props. If you really want to set a prop value in the state, you should use the componentWillMount hook. See docs.
Although it seems like you're working against the framework here. When you want to change a prop value, the parent component should react on this, and provide new props to the child component.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With