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; } }); var ToggleButton = React.createClass({ render: function() { return ; } });
Here
renders a
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 achecked
prop to a form field without anonChange
handler. This will render a read-only field. If the field should be mutable usedefaultChecked
. Otherwise, set eitheronChange
orreadOnly
. Check the render method ofToggleButton
.
The
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; } }); var Intermediate = React.createClass({ render: function() { // Intermediate doesn't care of "text", but it has to pass it down nonetheless return ; } }); var Child = React.createClass({ render: function() { return {this.props.text}; } });
A solution exists to avoid that and automatically have the parent talking to its grandson without the child to pass properties manually, but it is still not officially fully supported by the React team: the context.
Context
Update 2015-09-22 : I thought since a while that I was missing this part, fixed now!
To solve this problem of hierarchy, a solution exists, the context.
The context is something that is still undocumented on the React documentation but that everybody is already using since a while, because of it so useful. But the team has still work to do that could break the current behavior, hence the undocumented part. Use it at your own risks keeping in mind that, one day, a release will potentially breaks the compatibility, and you’ll need to rewrite those parts.
The context is very useful when you have a dynamic tree of components, where things are moving, where you have a lot of properties to pass down : passing explicitely every props to every children is clearly not an option.
The concept of context is easy to understand:
– one component makes a JS object at disposal that will be available for _any_ of its children components, grand children and so on, without them to being passed down explicitely.
– any child can use this.context
to access this object (passed behind the scene by React)
But, to use it, you have to follow some rules :
– define getChildContext
on the parent to return what is the context (any JS object)
– define childContextTypes
on the parent to define what is the type of each property in this context (React.PropTypes.
)
– define contextTypes
on a (child) component that want to read from its context (which property does it want to read)
This will allow React to check the properties are properly exposed on runtime (dev only).
If we take back our previous example, but using context this time, we can avoid to modify the
component but still have
reading props from
:
var MyContainer = React.createClass({ getChildContext: function() { // it exposes one property "text", any of the components that are // rendered inside it will be able to access it return { text: 'Where is my son?' }; }, // we declare text is a string childContextTypes: { text: React.PropTypes.string }, render: function() { // no props to pass down return; } }); var Intermediate = React.createClass({ render: function() { // this component has no props return ; } }); var Child = React.createClass({ // we declare we want to read the .text property of the context contextTypes: { text: React.PropTypes.string }, render: function() { // this component has access to the current context // exposed by any of its parent return {this.context.text}; } });
You can see this code in action on this jsbin.
But even if that works, try to be sure there is no other way and that is the best way for you.
Just as a side node, react-router is using it internally.
How a ownee can talk to its owner
Now, let’s say the
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 ; } });
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
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; } }); 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 ; } });Are you checked ? {this.state.checked ? 'yes' : 'no'}
And the compiled version of the
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
and display the sum of checked input in the container :
var MyContainer = React.createClass({ getInitialState: function() { return { checked: false, 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; } }); 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 ; } });How many are checked ? {this.state.totalChecked}
That was pretty easy to do that right ? We just added totalChecked
instead of checked
on
and update it when a child changes. The callback we pass is the same for every
.
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
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} }); // 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 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{this.props.name}; } }); React.render(, 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 better 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, Facebook Relay with GraphQL, or Redux, those are very handy.
Pingback: 萌の宇 – React生命周期、API和深入用法()