Build a user interface with React

ECA React - December 5th 2018

Last time...

  • Render classes dynamically

  • Render lists

  • Conditional rendering

  • Handling events

  • Composing components

  • Passing data to components

  • State vs props

State

  • Local to the component
  • Other components cannot access it

Props

  • Passed to a component
  • Once passed to the component, it cannot be modified

VS

Let's continue !

+

-

3

+

-

+

-

+

-

1

+

-

Total

10

Reset

Delete

Delete

Delete

Delete

Delete

Zero

Zero

6

Zero

Zero

Increment

5

2

3

Increment

Increment

Increment

Increment

Passing data to components

  • Data can be passed through attributes

  • <PokeCounter key={counter.id} count={counter.count} anotherAttribute="a value I pass to my component"/>
  • You can then retrieve the data from a plain JavaScript object called props

  • key will not be passed as it is a special keyword used to identify elements in a list

pokeCounter.jsx

import React, { Component } from "react";
import PokeCounter from "./pokeCounter";

class PokeCounters extends Component {
  state = {
    counters: [
        { id: 1, value: 0 }
        { id: 2, value: 0 },
        { id: 3, value: 0 },
        { id: 4, value: 0 },
        { id: 5, value: 0 }
    ]
  };

  render() {
    return (
        <div>
            {this.state.counters.map(counter => 
                <PokeCounter 
                    key={counter.id} 
                    value={counter.value}
                />
            )}
        </div>
    );
  }
}

export default PokeCounters;
import React, { Component } from "react";

class PokeCounter extends Component {
  state = {
    value: this.props.value
  };

  /** [...] */

  incrementCount = () => {
    this.setState({ 
        value: this.state.value + 1 
    });
  };

  render() {
    return (
      <React.Fragment>
        <span 
            className={this.getBadgesClasses()}>
            {this.formatCount()}
        </span>
        <button 
            onClick={this.incrementCount} 
            className="btn btn-secondary btn-sm"
        >
        Increment
        </button>
      </React.Fragment>
    );
  }
}

export default PokeCounter;

pokeCounters.jsx

  • In case of a complex element, you can pass data as children props

  • <PokeCounter key={counter.id} count={counter.count}>
        <h1>Hello world</h1>
    </PokeCounter>
  • data is then accessible in props.children

React dev tools

Available in Chrome and Firefox

Raising and handling events

2

Increment

3

5

0

0

Delete

Increment

Delete

Increment

Delete

Increment

Delete

Increment

Delete

Let's add "Delete" button

PokeCounters

PokeCounter

state = {

  counters: []

};

Delete button

How to update that state?

The component that owns a piece of state should be the one modifying it

PokeCounters

PokeCounter

Raise the event: delete

Handle the event: delete

PokeCounters

PokeCounter

this.props.onDelete

handleDelete

<PokeCounter onDelete={this.handleDelete}>

class PokeCounters extends Component {
  /** [...] */

  handleDelete = () => {
    console.log("Event handler called");
  };

  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <PokeCounter
            key={counter.id}
            value={counter.value}
            onDelete={this.handleDelete}
          />
        ))}
      </div>
    );
  }
}
class PokeCounter extends Component {
  
  /** [...] */
  render() {
    return (
      <div>
        /** [...] */
        <button
          className="btn btn-danger btn-sm-m-2"
          onClick={this.props.onDelete}
        >
          Delete
        </button>
      </div>
    );
  }
}

Update the state

class PokeCounters extends Component {
  
  /** [...] */

  handleDelete = counterId => {
    const counters = this.state.counters.filter(c => c.id !== counterId);
    this.setState({ counters });
  };

  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <PokeCounter
            key={counter.id}
            value={counter.value}
            id={counter.id}
            onDelete={this.handleDelete}
          />
        ))}
      </div>
    );
  }
}
class PokeCounter extends Component {
  
  /** [...] */
  render() {
    return (
      <div>
        /** [...] */
        <button
          className="btn btn-danger btn-sm-m-2"
          onClick={() => this.props.onDelete(this.props.id)}
        >
          Delete
        </button>
      </div>
    );
  }
}
class PokeCounters extends Component {
  /** ... */

  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <PokeCounter
            key={counter.id}
            counter={counter}
            onDelete={this.handleDelete}
          />
        ))}
      </div>
    );
  }
}
class PokeCounter extends Component {
  state = {
    value: this.props.counter.value
  };

  /** ... */

  render() {
    return (
      <div>
        /** ... */
        <button
          className="btn btn-danger btn-sm-m-2"
          onClick={() => this.props.onDelete(this.props.counter.id)}
        >
          Delete
        </button>
      </div>
    );
  }
}

Single Source of Truth

2

Increment

3

5

0

0

Delete

Increment

Delete

Increment

Delete

Increment

Delete

Increment

Delete

Reset

Let's add "Reset" button

class PokeCounters extends Component {
  /** [...] */

  handleReset = () => {
    const counters = this.state.counters.map(c => {
      c.value = 0;
      return c;
    });
    this.setState({ counters });
  };

  render() {
    return (
      <div>
        <button
          className="btn btn-primary btn-sm-m-2"
          onClick={this.handleReset}
        >
          Reset
        </button>
        /** [...] */
        </div>
      </div>
    );
  }
}

It does not work because PokeCounter has a local state

Removing the local state

PokeCounters

PokeCounter

Controlled component

data (props)

raise events

import React, { Component } from "react";

class PokeCounter extends Component {
  formatCount() {
    return this.props.counter.value === 0 ? "Zero" : this.props.counter.value;
  }

  getBadgesClasses() {
    let classes = "badge m-2 badge-";
    classes += this.props.counter.value === 0 ? "warning" : "primary";
    return classes;
  }

  render() {
    return (
      <div>
        <span className={this.getBadgesClasses()}>{this.formatCount()}</span>
        <button
          onClick={() => this.props.onIncrement(this.props.counter)}
          className="btn btn-secondary btn-sm m-2"
        >
          Increment
        </button>
        <button
          className="btn btn-danger btn-sm-m-2"
          onClick={() => this.props.onDelete(this.props.counter.id)}
        >
          Delete
        </button>
      </div>
    );
  }
}

export default PokeCounter;
import React, { Component } from "react";
import PokeCounter from "./pokeCounter";

class PokeCounters extends Component {
  state = {
    counters: [
      { id: 1, value: 1 },
      { id: 2, value: 0 },
      { id: 3, value: 0 },
      { id: 4, value: 0 },
      { id: 5, value: 0 }
    ]
  };

  handleDelete = counterId => {
    const counters = this.state.counters.filter(c => c.id !== counterId);
    this.setState({ counters });
  };

  handleIncrement = counter => {
    const counters = [...this.state.counters];
    const index = counters.indexOf(counter);
    counters[index] = { ...counters[index] };
    counters[index].value++;
    this.setState({ counters });
  };

  handleReset = () => {
    const counters = this.state.counters.map(c => {
      c.value = 0;
      return c;
    });
    this.setState({ counters });
  };

  render() {
    return (
      <div>
        <button
          className="btn btn-primary btn-sm-m-2"
          onClick={this.handleReset}
        >
          Reset
        </button>
        <div>
          {this.state.counters.map(counter => (
            <PokeCounter
              key={counter.id}
              counter={counter}
              onDelete={this.handleDelete}
              onIncrement={this.handleIncrement}
            />
          ))}
        </div>
      </div>
    );
  }
}

export default PokeCounters;

Multiple components in sync

App

TotalBar

PokeCounters

PokeCounter

counters[]
import React, { Component, Fragment } from "react"
import TotalBar from "./components/totalBar"
import PokeCounters from "./components/pokeCounters"

class App extends Component {
  state = {
    counters: [
      { id: 1, value: 1 },
      { id: 2, value: 0 },
      { id: 3, value: 0 },
      { id: 4, value: 0 },
      { id: 5, value: 0 }
    ]
  }

  handleDelete = counterId => {
    const counters = this.state.counters.filter(c => c.id !== counterId)
    this.setState({ counters })
  }

  handleIncrement = counter => {
    const counters = [...this.state.counters]
    const index = counters.indexOf(counter)
    counters[index] = { ...counters[index] }
    counters[index].value++
    this.setState({ counters })
  }

  handleReset = () => {
    const counters = this.state.counters.map(c => {
      c.value = 0
      return c
    })
    this.setState({ counters })
  }

  render() {
    return (
      <Fragment>
        <TotalBar
          totalCount={this.state.counters.reduce(
            (acc, curr) => acc + curr.value,
            0
          )}
        />
        <main className="container">
          <PokeCounters
            counters={this.state.counters}
            onReset={this.handleReset}
            onIncrement={this.handleIncrement}
            onDelete={this.handleDelete}
          />
        </main>
      </Fragment>
    )
  }
}

export default App
import React, { Component } from "react"
import PokeCounter from "./pokeCounter"

class PokeCounters extends Component {
  render() {
    const { counters, onReset, onIncrement, onDelete } = this.props

    return (
      <div>
        <button className="btn btn-primary btn-sm-m-2" onClick={onReset}>
          Reset
        </button>
        <div>
          {counters.map(counter => (
            <PokeCounter
              key={counter.id}
              counter={counter}
              onDelete={onDelete}
              onIncrement={onIncrement}
            />
          ))}
        </div>
      </div>
    )
  }
}

export default PokeCounters
import React, { Component } from "react"

class PokeCounter extends Component {
  formatCount() {
    return this.props.counter.value === 0 ? "Zero" : this.props.counter.value
  }

  getBadgesClasses() {
    let classes = "badge m-2 badge-"
    classes += this.props.counter.value === 0 ? "warning" : "primary"
    return classes
  }

  render() {
    return (
      <div>
        <span className={this.getBadgesClasses()}>{this.formatCount()}</span>
        <button
          onClick={() => this.props.onIncrement(this.props.counter)}
          className="btn btn-secondary btn-sm m-2">
          Increment
        </button>
        <button
          className="btn btn-danger btn-sm-m-2"
          onClick={() => this.props.onDelete(this.props.counter.id)}>
          Delete
        </button>
      </div>
    )
  }
}

export default PokeCounter
import React from "react"

const TotalBar = ({ totalCount }) => {
  return (
    <nav className="navbar navbar-light bg-light">
      <div>
        Total: <span className="badge m-2 badge-info">{totalCount}</span>
      </div>
    </nav>
  )
}

export default TotalBar

Stateless functional components

Exercice

3

Zero

6

1

Total

10

Reset

Delete

Delete

+

-

3

+

-

+

-

+

-

1

+

-

Total

10

Reset

Delete

Delete

Delete

Delete

Delete

Increment

Increment

Zero

Delete

Increment

Zero

Zero

6

Delete

Increment

Delete

Increment

Exercice

  • Add Pokeball images
  • Align in columns
  • Create "-" (decrement) button
    • It should decrement corresponding value
    • It should be disabled when corresponding value is Zero

Correction

See you next wednesday!

React (3/5) - Raise and handle events

By zolani

React (3/5) - Raise and handle events

ECA React - December 5th 2018

  • 660