Communication between React Components with Signals
(Example is available on github).
Communicating between two react components is pretty simple if they have a child-parent / parent-child relationship. You use props as outlined in the official docs and in greater detail in this blog post.
But what if they do not have that relationship? An example could be a global loading bar positioned near the root of the HTML structure that is displayed anytime a request is being made to fetch data.
If the app is using stores you could use a store to trigger a re-render of a component. If the app is not using stores then another solution is to use a global event system.
The following simple example will use a signals system through the mini-signals package. (To see a comparison between different systems like signals and pub/sub see here).
The Application⌗
This app consists of two components: a loading bar component, and a component with two buttons to show and hide the loading bar.
LoadingBar component⌗
import MiniSignal from 'mini-signals';
import React from 'react';
// used to dispatch signals to this component from other components
export const loadingBarSignal = new MiniSignal();
export class LoadingBar extends React.Component {
constructor(props) {
super(props);
this.state = {
show: false
};
this.changeDisplay = this.changeDisplay.bind(this);
}
changeDisplay(bool) {
this.setState({
show: bool
});
}
componentDidMount() {
//add signal listener
this.binding = loadingBarSignal.add(this.changeDisplay);
}
componentWillUnmount() {
this.binding.detach();
}
render() {
let inlineStyle = (this.state.show === false) ? {display:'none'} : {};
return (
<progress style={inlineStyle}>Loading...</progress>
)
}
}
A progress
HTML element is used. This component has a state property named show
. When this property changes the render method will possibly update the inline css.
The first step loadingBarSignal
a new MiniSignal is created and made exportable so other files can send this component signals. In componentDidMount
we create a listener for this signal that will call the changeDisplay
method that calls setState
and therefore may re-render the component.
The signal is detached when the component is unmounted.
Buttons component⌗
import React from 'react';
import {loadingBarSignal} from './LoadingBar';
export default class Buttons extends React.Component {
constructor(props) {
super(props);
this.displayLoadingBar = this.displayLoadingBar.bind(this);
}
displayLoadingBar(bool) {
loadingBarSignal.dispatch(bool);
}
render() {
return (
<div>
<button onClick={this.displayLoadingBar.bind(this, true)}>Show loading bar</button>
<button onClick={this.displayLoadingBar.bind(this, false)}>Hide loading bar</button>
</div>
)
}
}
The Buttons component imports loadingBarSignal
from the LoadingBar
component so it can communicate. When either of the buttons are clicked a signal will be dispatched to the LoadingBar component that may make cause its state to change.
Testing⌗
Here is an example unit test for this behaviour :
import {expect} from 'chai';
import jsdom from 'mocha-jsdom'
import React from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-addons-test-utils';
import {loadingBarSignal, LoadingBar} from '../components/LoadingBar';
describe('Loading Bar Component', function () {
jsdom();
before(function () {
this.component = TestUtils.renderIntoDocument(<LoadingBar />);
this.renderedDOM = () => ReactDOM.findDOMNode(this.component);
});
it('should not visible by default', function () {
expect( this.renderedDOM().getAttribute('style').replace(/ /g,'') ).to.contain('display:none');
expect(this.component.state.show).to.be.false;
});
it('should be able to update visibility by sending a signal', function () {
loadingBarSignal.dispatch(true);
expect( this.renderedDOM().getAttribute('style').replace(/ /g,'') ).to.not.contain('display:none');
expect(this.component.state.show).to.be.true;
loadingBarSignal.dispatch(false);
expect( this.renderedDOM().getAttribute('style').replace(/ /g,'') ).to.contain('display:none');
expect(this.component.state.show).to.be.false;
});
});