If you are not yet confortable with the notion of ownership and parent/child in React, take a look at my previous post.
How to communicate between React components ? That’s a good question, and there are multiple answers. That depends of the relationship between the components, and then, that depends on what you prefer.
I am not talking about data-stores, data-adapters or this kind of data-helpers that gets data from somewhere that you need to dispatch to your components, I’m really just talking about communication between React components only.
There are 3 possible relationships:
How an owner can talk to its ownee
That the easiest case actually, very natural in the React world and you are already using it.
You have a component that renders another one and passing it some props.
var MyContainer = React.createClass({ getInitialState: function() { return { checked: true }; }, render: function() { return <ToggleButton text="Toggle me" checked={this.state.checked} />; } }); var ToggleButton = React.createClass({ render: function() { return <label>{this.props.text}: <input type="checkbox" checked={this.props.checked} /></label>; } });
Here <MyContainer>
renders a <ToggleButton>
passing it a checked
property. That’s communication.
You just have to pass a prop to the child component your parent is rendering.
By the way, in my example, notice that clicking on the checkbox has no effect:
Warning: You provided a <code>checked</code> prop to a form field without an <code>onChange</code> handler. This will render a read-only field. If the field should be mutable use <code>defaultChecked</code>. Otherwise, set either <code>onChange</code> or <code>readOnly</code>. Check the render method of <code>ToggleButton</code>.
The <ToggleButton>
has its this.props
set by it parent, and only by it (and they are immutable).
Hierarchy problem
One disavantage of this technique is if you want to pass down a prop to a grandson (you have a hierarchy of components): the son has to handle it first, then pass it to the grandson. So with a more complex hierarchy, it’s going to be impossible to maintain. Example with just one intermediate :
var MyContainer = React.createClass({ render: function() { return <Intermediate text="where is my son?" />; } }); var Intermediate = React.createClass({ render: function() { // Intermediate doesn't care of "text", but it has to pass it down nonetheless return <Child text={this.props.text} />; } }); var Child = React.createClass({ render: function() { return <span>{this.props.text}</span>; } });
How a ownee can talk to its owner
Now, let’s say the <ToggleButton>
controls its own state and wants to tell its parent it has been clicked, for the parent to display something. Thus, we add our initial state and we add an event handler on the change
event of our input:
var ToggleButton = React.createClass({ getInitialState: function() { return { checked: true }; }, onTextChanged: function() { console.log(this.state.checked); // it is ALWAYS true }, render: function() { return <label>{this.props.text}: <input type="checkbox" checked={this.state.checked} onChange={this.onTextChanged}/></label>; } });
Notice that because I don’t change the state of this.state.checked
in onTextChanged
, the value is always true. React doesn’t handle the toggle of your own value just because it’s a input checkbox, it just notify you but nothing truly changed.
Therefore we add the state change and this is where we would like to callback our parent right ?
onTextChanged: function() { this.setState({ checked: !this.state.checked }); // callbackParent(); // ?? },
To have a reference to a callback pointing to the parent, we are simply going to use the first way we talked about : owner to ownee (parent to child) communication. The parent will pass a callback through a prop: we can pass anything through them, they are not DOM attributes, they are pure Javascript object.
Here is an example where the <ToggleButton>
notify its owner its state changed. The parent listens to this event and change its own state too to adapt its message :
var MyContainer = React.createClass({ getInitialState: function() { return { checked: false }; }, onChildChanged: function(newState) { this.setState({ checked: newState }); }, render: function() { return <div> <div>Are you checked ? {this.state.checked ? 'yes' : 'no'}</div> <ToggleButton text="Toggle me" initialChecked={this.state.checked} callbackParent={this.onChildChanged} /> </div>; } }); var ToggleButton = React.createClass({ getInitialState: function() { // we ONLY set the initial state from the props return { checked: this.props.initialChecked }; }, onTextChanged: function() { var newState = !this.state.checked; this.setState({ checked: newState }); this.props.callbackParent(newState); // hey parent, I've changed! }, render: function() { return <label>{this.props.text}: <input type="checkbox" checked={this.state.checked} onChange={this.onTextChanged}/></label>; } });
And the compiled version of the <MyContainer>
where we can see the different props passed by in a classic JS object:
return React.createElement("div", null, React.createElement("div", null, "Are you checked ? ", this.state.checked ? 'yes' : 'no'), React.createElement(ToggleButton, {text: "Toggle me", initialChecked: this.state.checked, callbackParent: this.onChildChanged}) );
Here is the result :
When I click on the input, the parent gets notified, and changes its message to ‘yes’.
We have the same problem than before : if you have intermediate components in-between, you have to pass your callback through the props of all of the intermediate components to get to your target.
More details about the React event system
In the event handler onChange
and any other React events, you will have access to :
this
: this is your component- one argument, which is the event from React : a
SyntheticEvent
. Here is what it looks like :
All events managed by React have nothing to do with the default javascript onclick/onchange
we used to know. React has its own implementation. Basically, they bind every events on the body with a selector à la jQuery :
document.on('change', 'input[data-reactid=".0.2"]', function() { ... })
This code is not from React, it’s just an example to explain how they bind every events to the document.
If I’m not mistaken, the React code that truly handle the events is that :
var listenTo = ReactBrowserEventEmitter.listenTo; ... function putListener(id, registrationName, listener, transaction) { ... var container = ReactMount.findReactContainerForID(id); if (container) { var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container; listenTo(registrationName, doc); } ... // at the very of end of the listenTo inner functions, we can find: target.addEventListener(eventType, callback, false);
Here is the full list of the events React supports.
Using the same callback
Just for fun, let’s try with multiple <ToggleButton>
and display the sum of checked input in the container :
var MyContainer = React.createClass({ getInitialState: function() { return { totalChecked: 0 }; }, onChildChanged: function(newState) { // if newState is true, it means a checkbox has been checked. var newTotal = this.state.totalChecked + (newState ? 1 : -1); this.setState({ totalChecked: newTotal }); }, render: function() { return <div> <div>How many are checked ? {this.state.totalChecked}</div> <ToggleButton text="Toggle me" initialChecked={this.state.checked} callbackParent={this.onChildChanged} /> <ToggleButton text="Toggle me too" initialChecked={this.state.checked} callbackParent={this.onChildChanged} /> <ToggleButton text="And me" initialChecked={this.state.checked} callbackParent={this.onChildChanged} /> </div>; } }); var ToggleButton = React.createClass({ getInitialState: function() { return { checked: this.props.initialChecked }; }, onTextChanged: function() { var newState = !this.state.checked; this.setState({ checked: newState }); this.props.callbackParent(newState); // hey parent, I've changed! }, render: function() { return <label>{this.props.text}: <input type="checkbox" checked={this.state.checked} onChange={this.onTextChanged}/></label>; } });
That was pretty easy to do that right ? We just added totalChecked
instead of checked
on <MyContainer>
and update it when a child changes. The callback we pass is the same for every <ToggleButton>
.
Help me, my components are not related!
The only way if your components are not related (or are related but too further such as a grand grand grand son and you don’t want to mess with the intermediate components) is to have some kind of signal that one component subscribes to, and the other writes into. Those are the 2 basic operations of any event system: subscribe/listen to an event to be notify, and send/trigger/publish/dispatch a event to notify the ones who wants.
There are at least 3 patterns to do that. You can find a comparison here.
Here is a quick recap:
- Event Emitter/Target/Dispatcher : the listeners need to reference the source to subscribe.
- to subscribe : otherObject.addEventListener(‘click’, function() { alert(‘click!’); });
- to dispatch : this.dispatchEvent(‘click’);
- Publish / Subscribe : you don’t need a specific reference to the source that triggers the event, there is a global object accessible everywhere that handles all the events.
- to subscribe : globalBroadcaster.subscribe(‘click’, function() { alert(‘click!’); });
- to dispatch : globalBroadcaster.publish(‘click’);
- Signals : similar to Event Emitter/Target/Dispatcher but you don’t use any random strings here. Each object that could emit events needs to have a specific property with that name. This way, you know exactly what events can an object emit.
- to subscribe : otherObject.clicked.add(function() { alert(‘click’); });
- to dispatch : this.clicked.dispatch();
You can use a very simple system if you want, with no more option, it’s quite easy to write :
// just extend this object to have access to this.subscribe and this.dispatch var EventEmitter = { _events: {}, dispatch: function (event, data) { if (!this._events[event]) return; // no one is listening to this event for (var i = 0; i < this._events[event].length; i++) this._events[event][i](data); }, subscribe: function (event, callback) { if (!this._events[event]) this._events[event] = []; // new event this._events[event].push(callback); } } otherObject.subscribe('namechanged', function(data) { alert(data.name); }); this.dispatch('namechanged', { name: 'John' });
It’s a very simple EventEmitter but it does it job.
If you want to try the Publish/Subscribe system, you can use PubSubJS
React team is using js-signals, based on the Signals pattern, it’s pretty great.
Events in React
To use these events manager in React, you have to look at those 2 components functions : componentWillMount
and componentWillUnmount
.
You want to subscribe only if you component is mounted, thus you have to subscribe in componentWillMount
.
Same idea if you component is unmounted, you have to unsubscribe from events, you don’t want to process them anymore, you’re gone. Thus you have to unsubscribe in componentWillUnmount
.
The EventEmitter pattern is not very useful when you have to deal with components because we don’t have a reference to them. They are rendered and destroy automatically by React.
The pub/sub pattern seems adequate because you don’t need references.
Here is example where multiple products are displayed, when you click on one of them, it dispatches a message with its name to the topic “products”.
Another component (a brother in the hierarchy here) subscribes to this topic and update its text when it got a message.
// ProductList is just a container var ProductList = React.createClass({ render: function() { return <div> <ProductSelection /> <Product name="product 1" /> <Product name="product 2" /> <Product name="product 3" /> </div> } }); // ProductSelection consumes messages from the topic 'products' // and displays the current selected product var ProductSelection = React.createClass({ getInitialState: function() { return { selection: 'none' }; }, componentWillMount: function() { // when React renders me, I subscribe to the topic 'products' // .subscribe returns a unique token necessary to unsubscribe this.pubsub_token = pubsub.subscribe('products', function(topic, product) { // update my selection when there is a message this.setState({ selection: product }); }.bind(this)); }, componentWillUnmount: function() { // React removed me from the DOM, I have to unsubscribe from the pubsub using my token pubsub.unsubscribe(this.pubsub_token); }, render: function() { return You have selected the product : {this.state.selection}; } }); // A Product is just a <div> which publish a message to the topic 'products' // when you click on it var Product = React.createClass({ onclick: function() { // when a product is clicked on, we publish a message on the topic 'products' and we pass the product name pubsub.publish('products', this.props.name); }, render: function() { return <div onClick={this.onclick}>{this.props.name}</div>; } }); React.render(<ProductList />, document.body);
Here is the result :
Then clicking on Product 2 :
ES6: yield and js-csp
A new way to pass messages is to use ES6 with the generators (yield
). Take a look here if you’re not afraid : https://github.com/ubolonton/js-csp.
Basically, you have a queue and anyone having a reference to it can put objects inside.
Anyone listening will be ‘stuck’ until a message arrives. When that happens, it will ‘unstuck’ and instantly get the object and continues its execution. The project js-csp is still under development, but still, it’s another solution to this problem. More on that later ;-).
Conclusion
There is not a best solution. It depends on your needs, on the application size, on the numbers of components you have. For small application, you can use props and callback, that will be enough. Later, you can pass to pub/sub to avoid to pollute your components.
Here, we are not talking about data, just components. To handle data request, data changed etc. take a look at the Flux architecture with the stores, and Facebook Relay with GraphQL, it’s very handy.