In React, Data Only Goes One Way!
React apps are organized as a series of nested components. These components are functional in nature: that is, they receive information through arguments (represented in the props
attribute) and pass information via their return values (the return value of the render
function). This is called unidirectional data flow. Data is passed down from components to their children.
It's pretty easy to see how to pass information from a parent component to a child component that it is calling. We've already looked at how to use props
to do that.
But what about when you want something triggered in a child component to be reflected somewhere else in the UI? This is a very common situation.
Imagine, for instance, that I have a button component that changes the background color of its parent component when clicked. đź–Ś The code for the button component shouldn't know anything about the parent it is changing. So how does the parent component, which is responsible for rendering its own background color, know when the button was clicked?
The answer is for the parent component to pass one of its own functions as a callback in a prop when calling the child component. The child component can then call this when triggered, supplying the necessary information about its state or the users actions.
Essentially what we end up with is a child component that will call whatever callback the parent provides, when the user triggers an action.
Let's implement this example in our app to see how it works. We'll start with a static version of our markup. Add two new files to your app called ChangeColorButton.js
and ColoredBlock.js
. Here's the static code to go in each of them:
ChangeColorButton.js
:
import React from 'react';
class ChangeColorButton extends React.Component {
render() {
return (
<button >Change the color</button>
);
}
}
export default ChangeColorButton;
ColoredBlock.js
:
import React from 'react';
import ChangeColorButton from './ChangeColorButton.js';
class ColoredBlock extends React.Component {
render() {
return (
<div style={{height: '200px', width: '200px', backgroundColor: 'red'}}>
<ChangeColorButton ></ChangeColorButton>
</div>
)
}
}
export default ColoredBlock;
Now, to work with this in our create-react-app
environment, go to App.js
and change it to render ColoredBlock
:
App.js
:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ColoredBlock from './ColoredBlock.js';
class App extends Component {
render() {
return (
<div className="App">
<header><h1>My React App</h1></header>
<div className="main-content">
<ColoredBlock />
</div>
<footer>...</footer>
</div>
);
}
}
export default App;
Got it working? OK. Obviously, this isn't too exciting. The color doesn't really change when we click the button.
To make it work, the first step is to identify the parts of our UI that change based on user interactions. This is know as 'state', and it's super important in React.
In this case, it's pretty obvious. The only thing that is supposed to change so far is the color, so that's our only element of state right now.
The next step is to identify which components the state should live in. Usually an element of state will 'live' in the highest component that depends on it. In this case, that's the ColoredBlock
component.
Remaining with the static app, let's refactor now so that the hardcoded background color is now stored in the ColoredBlock
component's state
property:
ColoredBlock.js
:
import React from 'react';
import ChangeColorButton from './ChangeColorButton.js';
class ColoredBlock extends React.Component {
constructor(props) {
super(props);
this.state = {
backgroundColor: 'red'
};
}
render() {
return (
<div style={{height: '200px', width: '200px', backgroundColor: this.state.backgroundColor}}>
<ChangeColorButton ></ChangeColorButton>
</div>
)
}
}
export default ColoredBlock;
Now let's write a callback in ChangeColorButton.js
that handles the click event:
ChangeColorButton.js
:
import React from 'react';
class ChangeColorButton extends React.Component {
handleClick() {
console.log('clicked');
}
render() {
return (
<button onClick={this.handleClick}>Change the color</button>
);
}
}
export default ChangeColorButton;
Now, with the JavaScript console open, click on the button. You should see the click being logged.
The next step is to pass in the callback from the ColoredBlock
component that will handle the logic for ColoredBlock
to change its color. Let's try this:
ColoredBlock.js
:
import React from 'react';
import ChangeColorButton from './ChangeColorButton.js';
class ColoredBlock extends React.Component {
constructor(props) {
super(props);
this.state = {
color: 'red'
};
}
changeColor() {
let newColor = this.state.color === 'red' ? 'blue' : 'red';
this.setState({
color: newColor
});
}
render() {
return (
<div style={{height: '200px', width: '200px', backgroundColor: this.state.color}}>
<ChangeColorButton clickHandler={this.changeColor}></ChangeColorButton>
</div>
)
}
}
export default ColoredBlock;
Then, we'll call this handler in ChangeColorButton.js
. Let's leave the console.log
statement in there for now as well.
import React from 'react';
class ChangeColorButton extends React.Component {
handleClick() {
this.props.clickHandler();
console.log('clicked');
}
render() {
return (
<button onClick={this.handleClick}>Change the color</button>
);
}
}
export default ChangeColorButton;
Now, with the console open again, click the button.
Ack, what's this?
> Uncaught TypeError: Cannot read property 'props' of null
The problem here is the DOM. It likes to change the value of this
inside event handlers, changing this
to refer to the element that triggered the event. That can be useful sometimes, but it's not what we want here. In order to be able to refer to this
in our callback, we'll need to explicitly bind in on our instance. We can do that in the constructor function:
import React from 'react';
class ChangeColorButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.clickHandler();
console.log('clicked');
}
render() {
return (
<button onClick={this.handleClick}>Change the color</button>
);
}
}
export default ChangeColorButton;
Ok, keeping an eye on our console, let's try clicking now.
What, again??
> Uncaught TypeError: Cannot read property 'color' of undefined
This time it's in ColoredBlock.js
. Turns out we have a similar problem here, and the fix is the same. Let's explicitly bind this
in the constructor in ColoredBlock.js
:
constructor(props) {
super(props);
this.state = {
color: 'red'
};
this.changeColor = this.changeColor.bind(this);
}
It should work now!
Ok, let's review what's happening now. The state of our UI, its color, lives in ColoredBlock
, which makes sense because only ColoredBlock
needs to know what color it is to render our UI. Appropriately, our code only references the color inside ColoredBlock.js
.
Challenge:
What if our spec requires us to reference the current color of the background inside the button? Change the button component so that its text is just "I don't like red" when the current color is red, and "I don't like blue" when the current color is blue. Remember, ChangeColorButton should get all information about its parent's state through its props
attribute.
Solution:
ChangeColorButton.js
:
import React from 'react';
class ChangeColorButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.clickHandler();
}
render() {
return (
<button onClick={this.handleClick}>I dont like {this.props.currentColor}</button>
);
}
}
export default ChangeColorButton;
ColoredBlock.js
:
import React from 'react';
import ChangeColorButton from './ChangeColorButton.js';
class ColoredBlock extends React.Component {
constructor(props) {
super(props);
this.changeColor = this.changeColor.bind(this);
this.state = {
color: 'red'
};
}
changeColor() {
this.setState((prevState, props) => ({
color: prevState.color === 'red' ? 'blue' : 'red'
}));
}
render() {
return (
<div style={{height: '200px', width: '200px', backgroundColor: this.state.color}}>
<ChangeColorButton clickHandler={this.changeColor} currentColor={this.state.color}></ChangeColorButton>
</div>
)
}
}
export default ColoredBlock;
State updates may be asynchronous
You may have noticed that setState
is used in a different way above than in our ticking clock example. You might wonder why didn't we write it like this:
changeColor() {
this.setState({
color: this.state.color === 'red' ? 'blue' : 'red'
});
}
That's because state updates may be asynchronous, and you actually can't rely on this.state
or this.props
when calculating the new state. The solution is to use the alternate callback syntax for setState
.