🙄

【React】 React hooksについて

2023/05/19に公開

React Hooksとは

React hooksとは、状態管理やライフサイクルの機能をクラスを用いずに簡潔に記述できる機能になります。登場前はクラスコンポーネントで書くなければ行けなかったのに対し、React hooksによって関数コンポーネントで記述できるようになりました。Hooksにより、React conceptsである、props, state, context, refs and lifecycleをAPIとして利用することができます。

useState[1]

useStateとは、React stateを関数コンポーネントとして追加できる機能になります。
こちらが簡単なuseStateのになります。

import React, { useState } from 'react';

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

こちらがuseStateの宣言部分になります。useStateはデータを保持するstateと、その変数を更新するdispatch functionによって構成されているため、別々に受け取ります。こちらの[x, y]は、JSでdestructuringと呼ばれています。useStateには、初期値をいれることができ、useStateのコンストラクタは、最初のレンダー時にのみ呼び出されます。注意点としては、if分やfor分の中では呼び出すことができず、必ず関数コンポーネントのトップレベルで呼ぶ必要があります。

const [count, setCount] = useState(0);

useStateの引数には、Stringやintなどの単一変数に加え、次のように関数を渡すことができます。


const [count, setCount] = React.useState(initialState('arg'))

しかしながら、このように関数を渡すと、initialStateは、ページがレンダーされる旅に呼び出されます。useStateなどで値の更新があった場合など全てです。それにより、initialStateがもしcomputational expensiveであった場合かなりの負荷がかかることになります。

この解決策としてLazy Initializationがあります。

const [count, setCount] = React.useState(() => 0)

Lazy Initialization

Lazy Initializationでは、初期化部分をアロー関数に置き換え、本体でその関数を呼び出すことで、ページ読み込み時の一度しか実行されません。

const [count, setCount] = useState(() => initialState())

useEffect

useEffectとは、関数コンポーネント内で副作用を実行するためのHookになります。副作用はレンダリングに関係のない処理、つまりコンポーネントの出力には関係の無い処理であるため、useEffectによりレンダリングと副作用を切り離すことができます。

基本的な文法はこちらになります。

useEffect(callback[, dependencies]);
  • callback: 副作用の処理を記述します.レンダリングが終了した後に実行されます。

  • dependencies: 依存先の変数が格納されている配列になります。副作用の処理が実行されるタイミングを制御できます。こちらの変数は3つの方法で指定することができ、それぞれ実行されるタイミングが異なります。

  • depencenciesを指定しない場合
    callback関数は、全レンダリング実行後に実行されます。

useEffect(() => {
  console.log('hoge')
})
  • 依存する変数を指定した場合
    その格納された変数が変更された場合に、レンダリング後に実行されます。
useEffect(() => {
  console.log('hoge')
}, [count])
  • 空配列を渡す場合
    最初のレンダリング後の一回のみ実行されます。
useEffect(() => {
  console.log('hoge')
}, [])

クリーンアップ関数

次のようにしてクリーンアップ関数も登録すことができます。こちらはReact LifecycleのcomponentWillUnmount()に対応します。

useEffect(() => {
    window.addEventListener("resize", this.handleResize);
    return () => {
        // componentwillunmount in functional component.
        // Anything in here is fired on component unmount.
	window.removeEventListener("resize", this.handleResize);
    }
}, [])

ケーススタディ

こちらの内容は次の記事を参考に、個人学習のためにまとめています。
https://zenn.dev/uhyo/articles/useeffect-taught-by-extremist

  • イベントハンドラの登録
    ページ全体にたいしてイベントに応じて、DOMを操作するようなイベントハンドラを登録する時に利用できます。

  • APIによるデータ取得
    APIを利用して外部から情報を取得し、stateを更新し表示するケース。クリーンアップ関数を定義していれば問題ありません。しかしながら、APIによって得られた情報がUIに関係のない部分である場合は好ましくありません。コンポーネント内に記述されたロジックに利用される場合は、適切ではありません。

  • ユーザーの入力などによるイベントのトラッキング

useEffect(() => {
    track("search", { searchQuery });
  }, [searchQuery]);

この場合は、クリーンアップ処理を記述することができないため、このような更新系の処理をすることは適切ではありません。また依存配列であるsearchQueryがロジックに関わっていることも不適切です。

TODO:
https://react.dev/learn/you-might-not-need-an-effect

  • PropsやStateを用いて、Stateをアップデートする場合
間違った書き方
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}
js
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  const fullName = firstName + ' ' + lastName;
  // ...
}

既定のpropsやstateで値を更新できる場合、レンダリング途中で実行すべきでuseEffectを使うべきではありません。

  • Computatinally Expensiveな処理を実行する時
シンプルに実装した場合
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ This is fine if getFilteredTodos() is not slow.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

getFiltererdTodosがスローな場合は問題ありません。しかしながら、トップレベルで記述しているため、関係のない別のstateが更新された場合にも、実行されてしまいます。その場合は、useMomeを用いて結果をキャッシュするべきです。しかしながら、結果はstoreされるためメモリ消費が大きくなる場合もあります。

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Does not re-run getFilteredTodos() unless todos or filter change
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

https://react.dev/learn/you-might-not-need-an-effect

useContext

親コンポーネントから子コンポーネントへの値の受け渡しには、propsオブジェクトが利用されますが、複数のコンポーネントを介してデータを渡す場合には複雑となるため、useContextを利用することで、異なる階層のコンポーネントでデータの共有が行えます。

使い方

まずは親コンポーネントでContextの作成を行います。

Main.js
import { createContext } from 'react';

export const UserCount = createContext()

次に関数コンポーネント内で、渡したいコンポーネントの上位コンポーネントを囲む形でProviderを定義し値を渡します。

Main.js
function (){
  return (
    <div>
      <h1>Learn useContext</h1>
      <UserCount.Provider value={100}>
        <ComponentA/>
      </UserCount.Provider>
    </div>
  );
}

次は受け取り手の設定です。CreateしたContextと受け取るためのReact Hooksをインポートし次のように受け取ります。

import { useContext} from 'react'
import { UserCount } from '../Main'

const ComponentC = () => {
    const count = useContext(UserCount)
    return (
        <div>
            <p>Componet C</p>
            <p>{count}</p>
        </div>
    )
}

また数字などの値だけではなく、親コンポーネントのstateを渡すことも可能です。受け取りての実装は変わりません。

Main.js
function Main() {
    const [count, setCount] = useState(100);
    const value = {
        count,
        setCount,
    };
    return (
    <div className="App">
      <UserCount.Provider value={value}>
        <ComponentA />
      </UserCount.Provider>
    </div>
    )
}

汎用的にするために、Context用のコンポーネントも作成できます。

useReducer

こちらは、useStateと同様に状態管理が行えます。複数の関連するstateと状態変更ロジックがある場合に便利です。useReducerでは、オブジェクトや配列のstateを扱うことができます。基本的にはuseContextと併用されます。

const [state, dispatch] = useReducer(reducer,'初期値')

reducer: (state, action) => newStateを受け取る関数です。この関数では、stateに代入される値を、actionによって分けることが可能です。

dispathactionを引数に受け取り、実行することでstateを更新することができます。

//counterの初期値を0に設定
const initialState = 0

//reducer関数を作成
const reducerFunc = (countState, action)=> {
  switch (action){
    case 'increment':
      return countState + 1
    case 'reset':
      return initialState
  }
}

const Counter = () => {
  return (
    <>
      <ButtonGroup color="primary">
        <Button onClick={()=>dispatch('increment')}>increment</Button>
        <Button onClick={()=>dispatch('decrement')}>decrement</Button>
        <Button onClick={()=>dispatch('reset')}>reset</Button>
      </ButtonGroup>
    </>
  )
}

以下のように複数の関連するStateを更新する複雑な処理を記述できます。

複数のStateを更新する場合
const initialState ={
  firstCounter: 0,
  secondCounter: 100
}

const reducerFunc = (countState, action)=> {
  switch (action.type){
    case 'increment1':
      return {...countState, firstCounter: countState.firstCounter + action.value}
    case 'increment2':
      return {...countState, secondCounter: countState.secondCounter + action.value}
    case 'reset1':
      return {...countState, firstCounter: initialState.firstCounter}
    case 'reset2':
      return {...countState, secondCounter: initialState.secondCounter}
  }
}

const Counter2 = () => {
const [count, dispatch] = useReducer(reducerFunc, initialState)
  return (
    <>
      <h2>カウント:{count.firstCounter}</h2>
        <Button onClick={()=>dispatch({type: 'increment1', value: 1})}>increment1</Button>
        <Button onClick={()=>dispatch({type: 'reset1'})}>reset</Button>
        <Button onClick={()=>dispatch({type: 'increment2', value: 100})}>increment2</Button>
        <Button onClick={()=>dispatch({type: 'decrement2', value: 100})}>decrement2</Button>
        <Button onClick={()=>dispatch({type: 'reset2'})}>reset</Button>
      </ButtonGroup>
    </>
  )
}

useRef

脚注
  1. https://legacy.reactjs.org/docs/hooks-state.html ↩︎

Discussion