Make it to make it

いろいろ作ってアウトプットするブログ

React学習3(props, single source of truth, controlled component)

React復習がてら要点をまとめていく。

Single source of truth

Reactでの状態(state)管理は、一つのソースで行わないといけない。これがSingle source of truthと言われるものである。 あるlocal stateを持ったコンポーネントを複数読み込んでその親もstateを持つとすると、stateは親のコンポーネント管理下でなければいけない。

具体的な例として、counterコンポーネントが複数読み込まれているというケースを考える。

Counters.jsx(親コンポーネント)

import React, { Component } from 'react'
import Counter from './Counter'

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

  render() {
    const { counters } = this.state

    if (counters.length === 0) return <p>Nothing!</p>

    return (
      <>
        <button
          onClick={this.handleReset}
          className="btn btn-primary btn-sm m-2"
        >
          Reset
        </button>
        {counters.map(counter => (
          <Counter
            key={counter.id}
            value={counter.value}
            selected={true}
            onDelete={() => this.handleDelete(counter)}
          />
        ))}
      </>
    )
  }

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

    this.setState({
      counters,
    })
  }

  handleDelete = counter => {
    const { counters } = this.state
    const filteredCounters = counters.filter(c => {
      return c !== counter
    })

    this.setState({
      counters: filteredCounters,
    })
  }
}

export default Counters

Counter.jsx(子コンポーネント)

import React, { Component } from 'react'

class Counter extends Component {
  state = {
    count: this.props.value,
    selected: this.props.selected,
  }

  render() {
    return <>{this.renderCounter()}</>
  }

  handleIncrement = product => {
    this.setState({
      count: this.state.count + 1,
    })
  }

  renderCounter() {
    return (
      <div>
        <span className={this.getBadgeClasses()}>{this.formatCount()}</span>
        <button
          onClick={this.handleIncrement}
          className="btn btn-secondary btn-sm"
        >
          Increment
        </button>
        <button
          onClick={this.props.onDelete}
          className="btn btn-danger btn-sm m-2"
        >
          Delete
        </button>
      </div>
    )
  }

  getBadgeClasses() {
    let classes = 'badge m-2 badge-'
    classes += this.state.count === 0 ? 'warning' : 'primary'
    return classes
  }

  formatCount() {
    const { count } = this.state
    return count === 0 ? 'Zero' : count
  }
}

export default Counter

この例だとResetボタンが効かない。子コンポーネントと親コンポーネントの両方で重複したstateを持っているからだ。 そこでリファクタリングを行い、親コンポーネントに全てのstateを集約させる。 ちなみにこういう場合、子コンポーネントのことをControlled componentと呼ぶ。