2020-07-10

React Hooksを再確認する

Reactはクラスコンポーネントと、関数コンポーネントの2つ存在しており、大きく状態(state)があるかないかで、コンポーネントを使い分けていた
(状態があればクラスコンポーネント、なければ関数コンポーネント)

React 16.8からHooksという機能が追加され、状態のあるなしに関わらず全て関数コンポーネントで記述できるようになった
(関数コンポーネントでも状態を持つことができるようになった)

これにより従来のような、最初は見た目も記述もシンプルな関数コンポーネントでコンポーネントを作成し、状態を持つ必要が出てきたら、クラスコンポーネントにリファクタする
という作業が不要となり、状態を持つ必要が出てきてもHooksを使えば、関数コンポーネントのまま使うことができるようになった

今回はこのReact Hooksを再確認して、記事に自分の言葉で残しておく

useState

まず初めに状態管理で使用する useState についてみていく
必要最小限のコードは以下の通り

// reactからuseStateを読み込む
import React, { useState } from "react"

// 関数コンポーネントを定義
const myComponent = () => {
  // useState関数に状態の初期値となる値を渡し、戻り値として
  //   - 値(これが状態)
  //   - 値を書き換えることのできる関数
  // を受け取る(JSの分割代入をよくやる)
  //
  // この例では「初期値0のcountという状態を作成、その更新用関数と共に受け取る」
  // ということをやっている(console.logで確認するとよい)
  const [count, setCount] = useState(0)

  // countを+1したり-1したりする関数
  // これらの関数によってcount(状態)が書き換わると
  // Reactは新しい値を表示するためにコンポーネントの再描画を行う
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)

  return(
    // この <> はフラグメントと呼ばれるもので、DOMを囲んで1つにまとめることができる
    // returnで返すことのできるDOMは1つだけなので、
    // ここではdivとbuttonを1つにまとめるためにフラグメントで囲っている
    <>
      <div>現在のカウント: {count}</div>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </>
  )
}

export default myComponent

const [count, setCount] = useState(0) この部分はほぼ慣習として名前が決まっていて、
カウンターアプリを作るために count という状態を作ったのであれば、それを更新する関数は setCount
ECサイトを作るために price という状態を作ったのであれば、関数は setPrice となる

上の例では setCount(count + 1) のように引数に式を渡しているが、関数を渡すことも可能で、その場合は渡す関数の引数に、変更前の値を渡すことができる
具体的には以下の通り

// 引数に関数を渡すパターン
// setCount関数の第一引数の関数の引数は、変更前の値が渡っている
const increment = () => setCount(previousCount => previousCount + 1)

useState にはオブジェクトを渡すことも可能で、複数の状態を1つのオブジェクトに統合することができる

const props = {
  name: "",
  price: 1000,
}

const [state, setState] = useState(props)
console.log({state})

1

ただこれだといちいち state.namestate.price みたいに state を書かなければならないので

const [state, setState] = useState(props)
const { name, price } = state

のようにして分割代入を使って使いやすくしてもいいかもしれない

オブジェクトのプロパティのうち、いずれか1つだけを更新したい場合は、以下のように書く

setState({...state, name: "updatedName"})
// stateは { name: "updatedName", price: 1000 } となる
// setState({ name: "updatedName" }) だとpriceが欠落してしまうので注意

...state あたりは素のJSのお話なので、意味がわからない人は、以下のようにchromeのコンソールなどから試してみるとよい

const a = {name: 111, price: 999}
// => {name: 111, price: 999}

{...a}
// => {name: 111, price: 999}

{...a, name: 876}
// => {name: 876, price: 999}

関数で更新させたい場合は、 setState に関数への参照を渡し、その関数から同じようにオブジェクトを return すればよい

const updateFunc = (previousObject) => { ...previousObject, name: "updatedName"}
setState(updateFunc)

useEffect

従来クラスコンポーネントでしか使えなかった、ライフサイクルフックを関数コンポーネントでも使えるようにするための機能
useEffect は第一引数に関数を受け取り、Reactコンポーネントの描画が行われた後に、その関数を発火させる
そして useEffect 1つで、 componentDidMount componentDidUpdate componentWillUnmount の3つを賄うことができる(ゴイスー)

import React, { useEffect } from 'react'

useEffect(() => {
  console.log("componentDidMount や componentDidUpdate に相当する")

  // componentWillUnmountを模擬したい場合は関数を返してあげると、同じような動作となる
  return () => console.log("componentWillUnmount 的な処理")
})

↑だとコンポーネントのマウント時と、useState で作成した状態のいずれかが更新されると呼び出される
もし、 componentDidMount 相当の動作にしたい場合は、 useEffect の第二引数に空の配列を渡す

useEffect(() => {
  console.log("componentDidMount に相当する")
}, [])

もし、ある特定の状態が変更されたときだけコールバックが動作して欲しい場合は、以下のように useEffect の第二引数の配列にstateを指定する

const [name, setName] = useState("hoge")

useEffect(() => {
  console.log("マウント時とnameが更新されたときだけ発火する")
}, [name])

useEffect は定義にもよるが、割と頻繁に呼び出される可能性が高い機能であることを覚えておく

reducer

Reactにおいて状態遷移を管理するためのもの
(Reduxを使ったことがある人にはおなじみかもしれない)

stateaction をもとに state を返す
実態はただの関数で switch 文で action の識別子の条件分岐文が書かれていることがほとんど

ここで reducer を説明するために、シンプルなTODOアプリを作成するときのことを考えてみる
アプリには下記仕様があるものとする

  • 新しくTODOを作成できる
  • 指定したTODOを削除できる
  • 全てのTODOを一括削除できる

また状態として、作成されたTODOが入った配列があるものとする

このアプリに対して reducer を作成すると以下のようになる

// todosという名前でreducerを定義する
// 引数のstateはTODOの詰まった配列
const todos = (state = [], action) => {
  // actionのtypeを元に処理が分岐する
  switch(action.type) {
    case "CREATE_TODO":
      // 「新しくTODOを作成」したときの処理
      // actionにTODO1レコードに必要な情報が入ってるので、actionから取得して作成するTODOを準備
      const todo = { title: action.title, body: action.body }

      // IDを付与してTODOを作成するので、現在の状態から次に作成するTODOに付与するIDを算出する
      const length = state.length
      const id = length === 0 ? 1 : state[length - 1].id + 1

      // TODOが複数含まれている配列の最後に、作成したTODOを追加する
      return [...state, { id, ...todo }]
    case "DELETE_TODO":
      // 「指定したTODOを削除」したときの処理
      // return 〜〜削除処理を実装する〜〜
    case "DELETE_ALL_TODOS":
      // 「全てのTODOを一括削除」したときの処理
      // 単純にstateを空にすればいいので、空の配列を返す
      return []
    default:
      return state
  }
}

export default todos

これを実際に使うと以下のようになる

// useReducerを読み込む
import React, { useReducer } from 'react'
import reducer from "./reducers"

// useReducerには、第一引数としてreducerを、第二引数に初期状態を、第三引数に初回のみ発火するコールバック関数を渡すことができる
// reducerは↑で作成したreducer(todos)で、TODOは配列で管理したいので、空の配列を渡している
const [state, dispatch] = useReducer(reducer, [], )

これで準備ができたので、ボタンが押されたときの処理に動作を追加していく

const addTodo = () => {
  // reducerを呼び出している
  // titleとbodyは(ここでは書いてませんが)useStateで管理されているinputタグの値
  dispatch({
    type: "CREATE_TODO",
    title,
    body,
  })
}

const deleteAllTodo = () => {
  // titleやbodyを渡す必要はない
  dispatch({ type: "DELETE_ALL_TODOS" })
}

<button onClick={addTodo}>TODOを作成する</button>
<button onClick={deleteAllTodo}>全てのTODOを削除する</button>

これでTODOを作成・全削除する機能が作成できた
削除処理なども基本的な実装フローは同じなので、あとは必要に応じてaction.typeを追加して、目的の状態へアプリを近づけていく

その他小ネタとか戒めとか

  • defaultProps

以下のようにデフォルトのpropsを関数コンポーネントに与えることができる

import React, { useState } from 'react'

const App = props => {
  const [name, setName] = useState(props.name)
  const [price, setPrice] = useState(props.price)

  return (
    <>
      <p>{name}{price}円です</p>
      <button onClick={() => setPrice(price + 1)}>+1</button>
      <button onClick={() => setPrice(price - 1)}>-1</button>
    </>
  )
}

App.defaultProps = {
  name: "商品A",
  price: 1000,
}

export default App
  • 定期的に繰り返す愚行

Rails + Reactの環境で、今までRailsが描画していた部分をReactで置き換えようとしたとき、
だいたいslimなりhamlなりでviewは書かれてるから、Railsが吐いた綺麗なHTMLをブラウザの検証ツールからDOMをコピペで持ってくることが多いんだけど、そのときによくやる愚行をちょっと書いておく(戒)

  • classclassName
  • forhtmlFor

この辺しょっちゅう怒られてる

  • jsxのreturnの中で forEach とか書き出す

returnに含められるのはタグだけです
JSのコードを適当に置いて使うことはできません

同じような要素を each で作りたい場合は事前に作って配置しましょう

// NG
return(
  <>
    hoge.forEach((elm) => {
      <div>{elm.name}</div>
    })
  </>
)

// OK
const elmList = hoge.map((elm) => {
  return <div>{elm.name}</div>
})

return(
  <>
    {elmList}
  </>
)

だいたい夕方、頭が回らなくなっているとやらかす