I'm trying to create a simple grid-based editor for a data structure and I'm having a couple conceptual problems with React.js. Their documentation is not very helpful on this, so I'm hoping someone here can help.

First, what is the correct way to transfer state from an outer component to an inner component? Is it possible to have state changes in the inner component "bubble up" to the outer component(s)?

Second, can two separate components share data, so that a mutation in one is visible in the other?

Below is a simplified example of the sort of thing I want to do (JSFiddle version):

I have a company object containing an array of employee objects. I want to lay out the employee list in an editable grid. When I click the button, I want to see the resulting company object, along with any mutations (writes to the console).

/** @jsx React.DOM */

var Cell = React.createClass({
    getInitialState: function () {
        return {data: ""};
    },

    componentWillMount: function () {
        this.setState({data: this.props.data});
    },
    onChange: function (evt) {
        console.log(this.state, evt.target.value);
        this.setState({data: evt.target.value});
    },
    render: function () {
        var data = this.props.data;
        return <input value={this.state.data} onChange={this.onChange} />
    }
});

var Row = React.createClass({
    render: function () {
        return (<div className="row">
            <Cell data={this.props.data.name} />
            <Cell data={this.props.data.location} />
            <Cell data={this.props.data.phone} />
        </div>);
    }
});

var Grid = React.createClass({
    render: function () {
        var rows = this.props.data.map(function (rowData, index) {
            return <Row key={index} data={rowData}>Row</Row>;
        });
        return <div className="table">{rows}</div>;
    }
});

var Button = React.createClass({
    getInitialState: function () {
        return {data: {}}
    },
    componentWillMount: function () {
        this.setState({data: this.props.data});
    },
    onClick: function () {
        console.log(this.state);
    },
    render: function () {
        return <button onClick={this.onClick}>Update</button>;
    }
});

var company = {
    name: "Innotech",
    employees: [
        {id: "1", name: "Peter", location: "IT", phone: "555-1212"},
        {id: "2", name: "Samir", location: "IT", phone: "555-1213"},
        {id: "3", name: "Milton", location: "loading dock", phone: "none"}
    ]
};


React.renderComponent(
    <div><Grid data={company.employees} /><Button data={company} /></div>,       
    document.getElementById('employees')
);
有帮助吗?

解决方案

I think this is the most underdocumented part of React right now. The suggested way to communicate between components is to simply set props when communicating from parent to child and to pass callbacks through props when communicating from child to parent.

When you feel that you want to share data between sibling components, it means that there should be a parent component managing the state and passing it to both components. Most of the time, your state should live near the top of your component hierarchy, and each piece of info should live in (at most) one component's state, not more.

For a bit more about this, see Pete Hunt's blog post, Thinking in React.


With this in mind, I've updated your fiddle.

I've changed Grid so that it doesn't maintain its own state but instead always displays the data passed via its props, and calls onCellChange from its props when it wants to request a change of the data from its parent. (The Grid component will expect its parent to update the grid's data prop with the modified data. If the parent refuses (perhaps because of failed data validation or similar), you end up with a read-only grid.)

You'll also notice that I created a new Editor component to wrap the grid and its sibling button. The Editor component now essentially manages the entire page. In a real app, it's likely that the contents of the grid would be needed elsewhere and so the state would be moved higher. I removed your Button component because it wasn't doing much beyond the native <button> tag; I left Cell but it too could be removed -- Row could easily use <input> tags directly.

Hope this makes sense. Feel free to ask if anything's unclear. There are usually also people around in the #reactjs IRC room if you want to chat more about any of this.

其他提示

I have been exploring ReactJS for the past week or so. My input to your question is asking a new question: why do you separate the Cell component from the Row and Grid components?

Coming from a Backbone background, Cell & Row & Grid makes sense, to have granular control over individual Cell Backbone.Views. However, it seems like that granular control & DOM update is what ReactJS tries to solve for you, which to me speaks for having a Grid component which implements a Row/Cell inside itself:

var Grid = React.createClass({
    onChange: function(evt, field) {
        this.props.data[field] = evt.target.value;
    },

    render: function () {
        var rows = this.state.data.map(function (rowData, index) {
            return (
                <div className="row" key={index}>
                    <input value={rowData.name} onChange={this.onChange.bind(null, "name")} />
                    <input value={rowData.location} onChange={this.onChange.bind(null, "location")} />
                    <input value={rowData.phone} onChange={this.onChange.bind(null, "phone")} />
                </div>
            );
        });

        return <div className="table">
                   {rows}
               </div>;
    }
});

(Ignore the on*Change handling, room for improvement there. Untested code)

The question is, would you ever re-use Cell or Row as individual components elsewhere? To me the answer is a "very likely no". In which case my solution above makes sense, IMHO, and gets rid of the problem of passing data and changes up & down.

Another way to share data between sibling components when a parent component doesn't make sense is to use events between components. For example, you can use Backbone.Events, Node.js Emitter ported to the browser or any similar lib. You can even use Bacon.js if you prefer reactive streams. There's a great and simple example of combine Bacon.js and React.js here : http://joshbassett.info/2014/reactive-uis-with-react-and-bacon/

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top