banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

03-リアクトフック

React Hook#

React Hook は React 16.8 の新機能です。これにより、クラスを作成せずに状態やその他の React 機能を使用できます。

React Hook を受け入れる#

Hook とは?#

Hook は、関数コンポーネント内で React の状態やライフサイクルなどの機能に「フック」することができる関数です。Hook はクラスコンポーネントでは使用できません。

いつ Hook を使用するのか?#

関数コンポーネントを作成していて、状態を追加する必要があることに気づいた場合、以前は他のコンポーネントをクラスに変換する必要がありましたが、今では既存の関数コンポーネント内で Hook を使用できます。

State Hook#

State Hook は、React の関数コンポーネントに状態を追加することを許可する Hook です。クラスでは、コンストラクタ内で this.state を設定することで状態を初期化できますが、関数コンポーネントでは this がないため、this.state を割り当てたり読み取ったりすることはできません。代わりに、コンポーネント内で useState を直接呼び出します。例えば:

import React, { useState } from 'react'

export default function Hello(prop) {
  const [name, setName] = useState('chanshiyu')
  const handleChange = e => setName(e.target.value)

  return (
    <div>
      <Input placeholder="あなたの名前" value={name} onChange={handleChange} />
    </div>
  )
}

useState は React が提供する新しいメソッドで、関数呼び出し時に変数を保存する方法です。これはクラス内の this.state が提供する機能と完全に同じです。一般的に、関数が終了すると変数は「消え」ますが、状態内の変数は React によって保持されます。

useState メソッドの唯一の引数は初期状態です。クラスの初期状態がオブジェクト型でなければならないのとは異なり、useState の引数は数字や文字列などの型であり、必ずしもオブジェクトである必要はありません。初期状態が複雑な計算によって得られる必要がある場合は、関数を渡してその中で計算し、初期状態を返すことができます。この関数は初回レンダリング時にのみ呼び出されます。

useState を呼び出すと、現在の状態と状態を更新する関数が返されます。配列の分割代入を使用して取得できます。クラスの this.setState とは異なり、状態変数の更新は常にそれを置き換え、マージするのではありません

もちろん、複数のフォームフィールドがある場合、最良の実装方法は Hook を抽出して再利用可能な関数にすることです:

import React, { useState } from 'react'

export default function Hello(prop) {
  const name = useFormInput('chanshiyu')
  const age = useFormInput('24')

  return (
    <div>
      <Input placeholder="あなたの名前" value={name.value} onChange={name.onChange} />
      <Input placeholder="あなたの年齢" value={age.value} onChange={age.onChange} />
    </div>
  )
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

初期値の計算が高コストである場合は、関数を渡すことができ、これにより一度だけ実行されます:

function Table(props) {
  // ⚠️ createRows() は毎回レンダリング時に呼び出されます
  const [rows, setRows] = useState(createRows(props.count))

  // ✅ createRows() は一度だけ呼び出されます
  const [rows, setRows] = useState(() => createRows(props.count))
}

Effect Hook#

Effect Hook は、関数コンポーネント内で副作用操作を実行することを可能にします。データ取得、購読の設定、React コンポーネント内の DOM の手動変更はすべて副作用に該当します。React コンポーネント内の一般的な副作用は、クリーンアップが不要なものと必要なものの 2 種類に分けられます。

クリーンアップが不要な Effect#

ここでは、クリーンアップが不要な副作用の例を示します。フォームの入力内容に基づいてページのタイトルを動的に変更します:

import React, { useState, useEffect } from 'react'

export default function Hello(prop) {
  const name = useFormInput('chanshiyu')

  const title = `こんにちは、${name.value}`
  useDocumentTitle(title)

  return (
    <div>
      <Input placeholder="あなたの名前" value={name.value} onChange={name.onChange} />
    </div>
  )
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title
  })
}

useEffect は、React コンポーネントがレンダリングされた後に特定の操作を実行する必要があることを伝えます。React は、渡された関数(これを「effect」と呼びます)を保存し、DOM 更新の後にそれを呼び出します。上記の例の effect では、渡された関数が document の title 属性を設定し、DOM 更新のたびにこの関数が呼び出されます。

useEffect をコンポーネント内に置くことで、effect 内で状態変数や他の props に直接アクセスできます。Hook は JavaScript のクロージャメカニズムを使用しており、それを関数のスコープ内に保持します

デフォルトでは、useEffect は毎回のレンダリング後に実行されます。もちろん、Effect をスキップしてパフォーマンスを最適化することもできますが、この部分については後で詳しく説明します。

useEffect に渡される関数は、毎回のレンダリングで異なる場合があります。これは意図的なものです。毎回の再レンダリングで新しい effect が生成され、以前のものと置き換えられます。ある意味で、effect はレンダリング結果の一部のようなものであり、各 effect は特定のレンダリングに「属します」

React クラスのライフサイクル関数に精通している場合、useEffect Hook を componentDidMount、componentDidUpdate、componentWillUnmount の 3 つの関数の組み合わせと考えることができます。 componentDidMount や componentDidUpdate とは異なり、useEffect でスケジュールされた effect はブラウザの画面更新をブロックしないため、アプリケーションがより迅速に応答するように見えます。ほとんどの場合、effect は同期的に実行する必要はありません。

クリーンアップが必要な Effect#

上記の動的にタブのタイトルを変更する副作用はクリーンアップが不要な副作用に該当しますが、イベントリスナーはクリーンアップが必要な副作用に該当します。メモリリークを防ぐために、クラスコンポーネントでは componentDidMount で追加したイベントリスナーを componentWillUnmount ライフサイクル内で削除しますが、これにより同じ機能ロジックを処理するコードが 2 つの異なる場所に分散されてしまい、これらの 2 つのコード部分が同じ副作用に作用します

関数コンポーネントでは、useEffect の処理方法がはるかに優れています。useEffect は、同じ副作用に属するコードを同じ場所で実行するように設計されています。effect が関数を返す場合、React はクリーンアップ操作を実行する際にそれを呼び出します。ここで、コンポーネントが読み込まれるときにウィンドウのリサイズイベントをリッスンし、コンポーネントが破棄されるときにそれを削除する例を示します:

import React, { useState, useEffect } from 'react'

export default function Hello(prop) {
  const width = useWindowWidth()

  return (
    <div>
      <div>幅: {width}</div>
    </div>
  )
}

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)
  const handleWindowResize = () => setWidth(window.innerWidth)

  useEffect(() => {
    window.addEventListener('resize', handleWindowResize, false)
    // ここで関数を返します。React はクリーンアップ操作を実行する際にそれを呼び出します
    return () => window.removeEventListener('resize', handleWindowResize)
  })

  return width
}

なぜ effect 内で関数を返す必要があるのでしょうか?これは effect のオプションのクリーンアップメカニズムです。各 effect はクリーンアップ関数を返すことができます。これにより、購読の追加と削除のロジックを一緒にまとめることができ、これらはすべて effect の一部となります。

Effect の関心事#

Effect Hook を使用する目的の 1 つは、クラス内のライフサイクル関数にしばしば無関係なロジックが含まれている問題を解決することですが、関連するロジックは異なるメソッドに分離されているという問題です。

Hook は、コードの用途に応じてそれらを分離することを許可します。ライフサイクル関数のように。React は、effect が宣言された順序に従ってコンポーネント内の各 effect を順番に呼び出します。新しい effect を呼び出す前に、前の effect をクリーンアップします

特定の状況では、毎回のレンダリング後にクリーンアップを実行したり、effect を実行したりすることがパフォーマンスの問題を引き起こす可能性があります。クラスコンポーネントでは、componentDidUpdate 内で prevProps または prevState の比較ロジックを追加することでこれを解決できます。

componentDidUpdate(prevProps, prevState) {
  if (prevState.name !== this.state.name) {
    document.title = `こんにちは、${this.state.name}`
  }
}

Effect Hook では、再実行が必要かどうかを判断するロジックがよりシンプルで、useEffect の Hook API に組み込まれています。配列を useEffect の 2 番目のオプション引数として渡すだけで、React は配列内の値が 2 回のレンダリングの間に変化したかどうかを判断し、effect の呼び出しをスキップするかどうかを決定します。これにより、パフォーマンスの最適化が実現されます。配列に複数の要素がある場合、1 つの要素が変化しただけでも、React は effect を実行します。

useEffect(() => {
  document.title = `こんにちは、${this.state.name}`
}, [name])

注意が必要です:この最適化方法を使用する場合は、配列に外部スコープで時間とともに変化し、effect 内で使用されるすべての変数が含まれていることを確認してください。そうしないと、コードが以前のレンダリングの古い変数を参照することになります。

一度だけ実行される effect(コンポーネントがマウントされるときとアンマウントされるときにのみ実行される)を実行したい場合は、空の配列([])を 2 番目の引数として渡すことができます。これにより、React に対して、あなたの effect が props や state のいかなる値にも依存していないことを伝え、したがって二度と繰り返し実行する必要がないことを示します。

空の配列([])を渡すと、effect 内の props と state は常にその初期値を持ち続けます。

React は、ブラウザが画面のレンダリングを完了するのを待ってから、useEffect を遅延呼び出しします。

もう 1 つのポイントは、effect の依存関係が頻繁に変化する場合、effect 内で setValue を使用する際に、値を渡すのではなく関数を渡すことができます:

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1) // この effect は `count` state に依存しています
    }, 1000)
    return () => clearInterval(id)
  }, []) // 🔴 バグ: `count` が依存関係として指定されていません

  return <h1>{count}</h1>
}

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1) // ✅ ここでは外部の `count` 変数に依存していません
    }, 1000)
    return () => clearInterval(id)
  }, []) // ✅ 私たちの effect はコンポーネントスコープ内のいかなる変数にも依存していません

  return <h1>{count}</h1>
}

Context Hook#

useContext は、context オブジェクト(React.createContext の戻り値)を受け取り、その context の現在の値を返します。useContext の引数は context オブジェクト自体でなければなりません。

useContext(MyContext) は、クラスコンポーネントの static contextType = MyContext または <MyContext.Consumer> に相当します。

現在の context 値は、上位コンポーネントの中で現在のコンポーネントに最も近い <MyContext.Provider> の value prop によって決まります。useContext を呼び出したコンポーネントは、context 値が変化するたびに再レンダリングされます。

import React, { useContext } from 'react'
import GlobalContext from '../../context'

export default function Hello(prop) {
  const local = useContext(GlobalContext)

  return (
    <div>
      <div>言語: {local}</div>
    </div>
  )
}

Reducer Hook#

以前の State Hook の紹介で、複数のフォームの useState を個別の関数に抽出して処理しました:

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue)
  const handleChange = e => setValue(e.target.value)

  return {
    value,
    onChange: handleChange
  }
}

これは useReducer の雛形で、React は状態を管理するために useReducer を内蔵しています。これは (state, action) => newState の形をした reducer を受け取り、現在の state とそれに対応する dispatch メソッドを返します。

状態のロジックが複雑で、複数のサブ値を含む場合や、次の状態が以前の状態に依存する場合は、useReduceruseState の代わりに使用できます。また、useReducer を使用することで、深い更新を引き起こすコンポーネントのパフォーマンスを最適化できます。

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState)

  function dispatch(action) {
    const nextState = reducer(state, action)
    setState(nextState)
  }

  return [state, dispatch]
}

呼び出し方:

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    // ... 他のアクション ...
    default:
      return state
  }
}

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer, [])

  function handleAddClick(text) {
    dispatch({ type: 'add', text })
  }
  // ...
}

Callback Hook#

useCallback は、インラインコールバック関数と依存関係の配列を引数として受け取り、そのコールバック関数のメモ化されたバージョンを返します。このコールバック関数は、特定の依存関係が変更されたときにのみ更新されます。最適化された子コンポーネントにコールバック関数を渡す際に、参照の等価性を使用して不必要なレンダリングを回避するのに非常に便利です(例えば shouldComponentUpdate)。

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])

useCallback(fn, deps)useMemo(() => fn, deps) に相当します。

依存関係の配列はコールバック関数に引数として渡されません。概念的には、すべてのコールバック関数で参照される値は依存関係の配列に含まれるべきです。

callback ref を使用して DOM を取得できます:

function MeasureExample() {
  const [height, setHeight] = useState(0)

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, []) // [] は useCallback の依存関係リストとして、これにより ref コールバックが再レンダリング時に変更されないことが保証されます

  return (
    <>
      <h1 ref={measuredRef}>こんにちは、世界</h1>
      <h2>上のヘッダーの高さは {Math.round(height)}px です</h2>
    </>
  )
}

または、再利用可能な Hook を個別に抽出することもできます:

function MeasureExample() {
  const [rect, ref] = useClientRect()
  return (
    <>
      <h1 ref={ref}>こんにちは、世界</h1>
      {/* ここで短絡演算を使用 */}
      {rect !== null && <h2>上のヘッダーの高さは {Math.round(rect.height)}px です</h2>}
    </>
  )
}

function useClientRect() {
  const [rect, setRect] = useState(null)
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect())
    }
  }, [])
  return [rect, ref]
}

Memo Hook#

useMemo はメモ化された値を返し、「作成」関数と依存関係の配列を引数として受け取ります。これは特定の依存関係が変更されたときにのみメモ化された値を再計算します。この最適化は、毎回のレンダリング時に高コストの計算を避けるのに役立ちます。依存関係の配列が提供されていない場合、useMemo は毎回のレンダリング時に新しい値を計算します。

useMemo に渡される関数はレンダリング中に実行されます。この関数内でレンダリングに関係のない操作を実行しないでください。副作用のような操作は useEffect の適用範囲に属し、useMemo には属しません。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])

Ref Hook#

useRef は可変の ref オブジェクトを返し、その .current プロパティは渡された引数(initialValue)で初期化されます。返された ref オブジェクトはコンポーネントのライフサイクル全体で不変です

const refContainer = useRef(initialValue)

公式の例を参照してください:

function TextInputWithFocusButton() {
  const inputEl = useRef(null)
  const onButtonClick = () => {
    // `current` は DOM にマウントされたテキスト入力要素を指します
    inputEl.current.focus()
  }

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>入力にフォーカス</button>
    </>
  )
}

useRef() と自作の {current: ...} オブジェクトの唯一の違いは、useRef は毎回のレンダリング時に同じ ref オブジェクトを返します

Ref Hook は DOM refs にのみ使用されるわけではありません。「ref」オブジェクトは、current プロパティが可変で任意の値を保持できる汎用コンテナであり、クラスのインスタンスプロパティに似ています。

function Timer() {
  const intervalRef = useRef()

  useEffect(() => {
    const id = setInterval(() => {
      console.log('tick')
    })
    // .current プロパティを使用してタイマー id を記録します
    intervalRef.current = id

    // コールバックはコンポーネントが破棄されるときにクリーンアップされます
    return () => {
      clearInterval(intervalRef.current)
    }
  })

  // または手動でクリーンアップできます
  function handleCancelClick() {
    clearInterval(intervalRef.current)
  }
}

前の props や state を保存するためにも使用できます:

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)
  return (
    <h1>
      現在: {count}, 前: {prevCount}
    </h1>
  )
}

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

概念的には、refs はクラスのインスタンス変数のようなものと考えることができます。遅延読み込みを行っていない限り、レンダリング中に refs を設定することは避けてください。これは予期しない動作を引き起こす可能性があります。逆に、通常はイベントハンドラーや effects 内で refs を変更するべきです。

ImperativeHandle Hook#

useImperativeHandle は、ref を使用する際に親コンポーネントに公開されるインスタンス値をカスタマイズすることを可能にします。useImperativeHandleforwardRef と一緒に使用する必要があります:

function FancyInput(props, ref) {
  const inputRef = useRef()
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  return <input ref={inputRef} />
}
FancyInput = forwardRef(FancyInput)

LayoutEffect Hook#

useLayoutEffectuseEffect と同じですが、すべての DOM 変更の後に同期的に effect を呼び出します。これを使用して DOM レイアウトを読み取り、再レンダリングを同期的にトリガーできます。ブラウザが描画を実行する前に、useLayoutEffect 内の更新計画は同期的にリフレッシュされます。視覚的な更新をブロックしないように、できるだけ標準の useEffect を使用してください

DebugValue Hook#

useDebugValue は、React 開発者ツールでカスタム hook のラベルを表示するために使用できます。

// 開発者ツールのこの Hook の横にラベルを表示します
// 例: "FriendStatus: Online"
useDebugValue(isOnline ? 'オンライン' : 'オフライン')

Hook のルール#

Hook は本質的に JavaScript 関数ですが、使用する際には 2 つのルールに従う必要があります:

  1. Hook は最上位でのみ使用してください。ループ、条件、またはネストされた関数内で Hook を呼び出さないでください。Hook が毎回のレンダリングで同じ順序で呼び出されることを確認してください。これにより、React は複数の useStateuseEffect の呼び出し間で hook 状態を正しく保持できます。
  2. React 関数内でのみ Hook を呼び出してください。通常の JavaScript 関数内で Hook を呼び出さないでください。

React は Hook の呼び出し順序に依存して、どの状態がどの useState に対応するかを判断しますので、毎回のレンダリング時の Hook の順序が一貫していることを確認してください。Hook の呼び出し順序が毎回のレンダリングで同じである限り、React は内部状態と対応する Hook を正しく関連付けることができ、正常に機能します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。