Make it to make it

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

React学習4(functional component)

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

React学習3で学んだところから、さらに階層深くコンポーネント構成をする、次のようなケースの場合。

.
├── App.jsx
├── components
│   ├── Counter.jsx
│   ├── Counters.jsx
│   └── Navbar.jsx
└── index.js

Objectやargument destructuringもふんだんに利用しながら記述していく。

App.jsx

import React, { Component } from 'react'
import _ from 'lodash'
import Navbar from './components/Navbar'
import Counters from './components/Counters'

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

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

    return (
      <div className="App">
        <Navbar totalCounters={counters.length > 0 && counters.length} />
        <div className="container">
          <Counters
            counters={counters}
            onReset={this.handleReset}
            onIncrement={this.handleIncrement}
            onDelete={this.handleDelete}
          />
        </div>
      </div>
    )
  }

  handleIncrement = counter => {
    const { counters } = this.state
    const clickedCounterIndex = counters.findIndex(c => c.id === counter.id)
    const countersIncremented = _.cloneDeep(counters)

    countersIncremented[clickedCounterIndex].value++

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

  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 App

Counters.jsx

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

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

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

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

export default Counters

Counter.jsx

import React, { Component } from 'react'

class Counter extends Component {
  render() {
    return <>{this.renderCounter()}</>
  }

  renderCounter() {
    const { onIncrement, onDelete } = this.props

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

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

  formatCount() {
    const { value } = this.props
    return value === 0 ? 'Zero' : value
  }
}

export default Counter

Navbar.jsx

stateを持たないコンポーネントに関しては、Stateless Functional Component (SFC) を用いて、次のような記述をする。 Lifecycle hooksに関しても、SFC内では使用することができない。

import React from 'react'

const Navbar = ({ totalCounters }) => {
  return (
    <>
      <nav className="navbar navbar-light bg-light">
        {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
        <a className="navbar-brand" href="#">
          Navbar
          <span className="badge badge-pill badge-info">{totalCounters}</span>
        </a>
      </nav>
    </>
  )
}

export default Navbar

ちなみに、class componentについてもstateを持たないことも当然できるので、単に Functional Component (FC) とのみ区別した方がより適しているという議論がある。