Fil d'Ariane
Mis à jour le mardi 7 mars 2017
  • 20 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Understand one-way data bindings

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

 

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 .

Exemple de certificat de réussite
Exemple de certificat de réussite