Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React JS this.props.data not defined in getInitialState for child component even though it is defined in render (and is defined in parent)

Scenario:

  1. Re-using components (<Row /> -> <Cell />) in the same grandparent wrapper (<ShippingTable /> -> <Grid />) for code re-use.
  2. For one, I assign an array of data and loop through to re-use child component (<Row /> -> <Cell />).
  3. For the second one, it's just an object (with the same properties as the array's objects) which I just assign directly in render (no need for this.props.data.map loop as already just one object).

Problem:

  1. For the array, all works as required. this.props.data is passed down to children, state is updated through various events and all is well.
  2. However, for the single object, all works fine until <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.
  3. Moreover, 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).
  4. Double moreover, if I force a UI re-render (e.g. change another state property in the grandparent <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'));
like image 593
Ted Avatar asked Mar 11 '15 20:03

Ted


1 Answers

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.

like image 199
rallrall Avatar answered Oct 02 '22 22:10

rallrall