LoginSignup
3
3

More than 3 years have passed since last update.

React hooks 【シュチュエーション別 hooks利用方法と使い方まとめ】

Posted at

モダンReactの代表になっているhooksですが、使いこなせるととても便利です。
今まで、クラスコンポーネントや
SFC(Stateless Functional Component)として高階コンポーネントやレンダープロップス、Recomposeなどを使っていた方は使い勝手の良さに驚くでしょう。

また、これからReactを学習する人に向けてもhooksを使いこなすことはとても価値があり、
FC(Function Component)+hooksから入門するべきだと思います。
実際の現場でclassやSFCはレガシーであり、FC+hooksへの移行が進んでいるからです。

復習がてら、どんなhooksがあるのか、どんな時にどう使うのかをまとめました。

前提として

React version 16.8.0 以降
hooksはFCでないと使用できない

Hooks.jsx
import React, { useState, useEffect, useContext } from 'react'


const hooks = () => {
  //関数コンポーネント内にhooksを書く

  return null
}

読み込みが必要ですがここから先ははしょりますので、上記のように読込んでください。
またはReact.hooksの名前でも可能です。

コンポーネント内で状態管理をしたい

useState

const hooks = () => {
  const [count, setCount] = useState(0)
  return(
    <>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>ボタン</button>
    </>
  )  
}

ステートフルな値と、それを変化させるための関数を分裂代入します。
上記は、ボタンを押すたびにsetCountによって状態が変化して数字が増えていきます。

また、useStateの第一引数はinitialState(初期値)となっていて、ロジックが含まれている場合は、関数を渡すこともできます。

  const [count, setCount] = useState(()=> 1*2*3)

オブジェクトの更新の仕方に注意

クラスコンポーネントの setState メソッドとは異なり、下記では正常にstateが更新されません。

const hooks = () => {
  const [user, setUser] = useState({ name: "test", age: "20" })
  return (
    <>
      <div>{user.name}</div>
      <button onClick={() => 
        setUser({user.name = 'testNEW'})
      }>
        ボタン
      </button>
    </>
  )
}

consoleでuserを出すとtestNEWが出力されてしまいます。
正常にnameの値のみ変更するためには、下記のようにスプレッド構文と併用してマージさせるように書きます。

const hooks = () => {
  return (
    <button onClick={() => 
      setUser({...user, ...{ name: "testNEW"}})
    }>
      ボタン
    </button>
  )
}

renderされた後に何か処理をしたい

useEffect

いままでcomponentDidMountをつかって、render後にAPIリクエストを行うといったアクションを取っていたと思いますが、useEffectを使ってそれを実現できます。

正確に言うとcomponentDidMountcomponentDidUpdatecomponentWillUnmount がまとまったものだと考えることができます。

const hooks = () => {
  const [user, setUser] = useState({ name: "test", age: "20" })

  //APIリクエストのサンプルです
  const sampleGetRequest = () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ name: "NEW" })
      }, 2000)
    })
  }
  //render後に発火
  useEffect(() => {
    sampleGetRequest().then(response => {
      setUser({ ...user, ...response })
    })
  }, [])

  return <div>{user.name}</div>
}

上記のコードを実行すると、renderされてから2秒後に
useStateで持っている、userが更新され、viewが書き換わります。

第二引数について

第二引数を引数なし、空の配列、値を入れた配列で使い分けることによって、3種類の使い方ができます。

引数なし
useEffect(() => {})

コンポーネントがマウントされた後、更新された後に関わらず、毎回のレンダー時に処理を実行します。
componentDidMount と componentDidUpdateを併用しているのと似ています。

空の配列
useEffect(() => {}, [])

コンポーネントが初回にマウントされた後のみ処理を実行します。
componentDidMountを記述しているのと似ています。

値を入れた配列
const [name, setName] = useState('test')
const [age, setAge] = useState(25)

useEffect(() => {
  console.log(`${name}${age}才だ`)
}, [count])

配列内に入れた値を監視してくれます。
上記の例だと、配列の中に記入したcountのデータの内容が書き換わった時に処理が実行されます。
なので、ageのデータ内容が更新されても何も起きず、countが更新された時のみlogが出るようになります。

無限ループに注意

引数を無し、配列に値を入れた引数の場合、無限ループになる可能性があります。
useEffectの説明の最初のコードの引数を無くしてみました。
これだと無限ループに陥って、処理を繰り返してしまいます。
レンダー => setUser({ ...user, ...response }) => レンダー => setUser({ ...user, ...response }) => レンダー => 続く...

const sampleGetRequest = () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ name: "NEW" })
      }, 2000)
    })
  }

useEffect(() => {
  sampleGetRequest().then(response => {     
    setUser({ ...user, ...response })
  })
})

なぜならsetUser({ ...user, ...response })によってuserの参照は毎回切れてしまうからですね。
{ name: "NEW", age: "20" }から{ name: "NEW", age: "20" }に変更されても、値は変わってないように見えて、参照は変わっているので、仮想DOMの差分を検知して再レンダーしてuseEffect発火という流れができてしまいます。
空の配列を引数に指定すれば治ります。
useEffect内で状態を変化させる際は気をつけましょう。

もっとuseEffectについて詳し知りたい方はuseEffect完全ガイドをご覧ください。

コンポーネントツリー内で、値を共有したい

useContext

通常だとデータを親から子に渡す際、propsとしてバケツリレーで渡していく必要があります。
ですが、useContextを使えば、バケツリレーの必要がなく、一気に下層コンポーネントに値を渡すことができます。
多くの階層を経由していくつかの props を渡すことを避けたいときはうってつけです。
クラスコンポーネントでもコンテクストは使えましたが、それのhooks版という感じです。
下記は、APP.jsxからTitle.jsxまで一気に(Header.jsxを飛び越えて)値を渡している例です。

App.jsx
import React, { createContext } from "react"
import Header from "./Header"

//最上部のコンポーネントではクラスコンポーネントと同じように、
//値を共有するためのコンテクストをcreateContextで作成
export const PageMetaContext = createContext("");

const App = () => {
  const PageMeta = {
    title: "タイトル",
    description: "詳細です"
  };

  return(
  //Header以下のツリーに共有できるようにプロバイダを使用、
  //valueに値を設定することで、ツリー内のどの子コンポーネントにも渡せる(今回は PageMeta)
    <PageMetaContext.Provider value={PageMeta}>
      <Header />
    </PageMetaContext.Provider>
  )
}

export default App;

値を使用する必要のないコンポーネントは何も特別なことはしなくてもいい

Header.jsx
import React from "react"
import Title from "./Title"

const Header = () => (
  <header>
    <Title />
  </header>
)

export default Header;

親のプロバイダで設定された値を使いたいコンポーネントのみuseContextを使う
注意すべき点は、useContext に渡す引数はコンテクストオブジェクト自体であること

Title.jsx
import React, { useContext } from "react";
//ツリーの親で作成されたコンテキストを読み込む
import { PageMetaContext } from "./App";

const Title = () => {
//ここでuseContextを使う
  const { title, description } = useContext(PageMetaContext);

  return (
    <>
      <h1>{title}</h1>
      <span>{description}</span>
    </>
  )
}

export default Title;

これで親で設定したPageMetaの中の文字列がviewに表示されます。

useContext に渡す引数に注意

useContext に渡す引数はコンテクストオブジェクト自体でなければいけません。

上記の例だと、titleのみ使いたいからといって、

const title = useContext(PageMetaContext.title);

にするとエラーが起きてしまうので気をつけましょう。

複数の複雑なstateを1つにして、templateをスッキリさせたい

useReducer

useReducerはuseStateと同じく、コンポーネント内で状態管理をするためのhookで、
useStateの状態管理をより堅牢であり、複雑なロジックが絡んだステートを更新するのに適しています。
Reduxに馴染みがあれば簡単ですが、初見だとわかりにくく使用するのにも気がひけるので順をおって説明します。

下記のようなテキストボックスに入力するした値をリストにできるサンプルをつくりました。

スクリーンショット 2020-06-20 19.58.12.png

2つのコンポーネントによって構成され、
下記はuseRecucerをもち、子コンポーネントからの入力によってリストをレンダーさせる親側のコンポーネントです。

Todo.jsx
import React, { useReducer } from "react"
import InputArea from "./InputArea"

const initialState = { input: "", items: [] }

const reducer = (state, action) => {
  switch (action.type) {
    case "updateInput":
      return { ...state, input: action.payload };
    case "resetInput":
      return { ...state, input: "" };
    case "addItem":
      return { ...state, items: [...state.items, action.payload] };
    case "removeItem":
      const filteredItems = state.items.filter(v => v.key !== action.payload);
      return {
        ...state,
        items: filteredItems
      };
    case "resetItems":
      return initialState;
    default:
      throw new Error();
  }
}

const Lists = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <ul>
        {state.items.map((item, index) => (
          <li key={index}>
            {item.title}
            <button
              onClick={() =>
                dispatch({ type: "removeItem", payload: item.key })
              }
            >
              削除
            </button>
          </li>
        ))}
      </ul>
      <InputArea input={state.input} dispatch={dispatch} />
    </div>
  )
}

export default Lists

まずuseReducerの宣言部分ですが、

const [state, dispatch] = useReducer(reducer, initialState);
  • state :ステートフルな値
  • dispatch :値を更新したいという旨をreducerに通知するための関数
  • useReducerの第1引数のreducer:値を更新するためのロジックが書いてある関数
  • useReducerの第2引数のinitialState:値の初期値、関数でもOK。

実際にレンダリングのために扱う値は、stateの中身になります。

値の更新は、reduceractionの2つの重要な役割によって行われます。

reducer

const reducer = (state, action) => {
  switch (action.type) {
    case "updateInput":
      return { ...state, input: action.payload };
    case "resetInput":
      return { ...state, input: "" };
    case "addItem":
      return { ...state, items: [...state.items, action.payload] };
    case "removeItem":
      const filteredItems = state.items.filter(v => v.key !== action.payload);
      return {
        ...state,
        items: filteredItems
      };
    case "resetItems":
      return initialState;
    default:
      throw new Error();
  }
};

reducer関数は、actionのtypeによって行う処理を分岐させるため、switch文で書きます。

引数の中身は

  • state:現在のステート
  • action:typeとpayloadが入ったオブジェクト
    • type:"updateInput"のような処理をするための名前
    • pyload:action関数から渡させる任意の値

今回のreducerの中には下記のしたい処理が書いてあります。

  • inputの値を更新
  • inputの値を空にする
  • 配列にinputの値を追加
  • 配列から、keyが同じオブジェクトを削除
  • inputと配列の内容を初期化
  • typeがない時エラーを投げる

それぞれのtypeごとに実行される関数から返される値は、新しいステートになります。
それにより、新しいレンダーが行われます。
そして何を元にtypeがわかり、処理が振分けされるのかというと、actionによって行われます。

action

dispatch({ type: "removeItem", payload: item.key })

actionは簡単にいうと、reducerへの更新依頼です。
useReducerで宣言時に代入されたdispatchを使うことで実現します。
{ type: "removeItem", payload: item.key }はreducer内のactionで使うことができます。

下位コンポーネントでstateの更新をさせる

下記は先ほどのTodo.jsxの子コンポーネントで、テキストボックスから値を入力し、stateを更新する役割を持っているコンポーネントです。

inputArea.jsx
import React from "react";

const InputArea = ({ input, dispatch }) => {
  const hundleChange = event => {
    dispatch({ type: "updateInput", payload: event.currentTarget.value });
  };

  const addItem = () => {
    dispatch({
      type: "addItem",
      payload: { title: input, key: new Date().getTime() + Math.random() }
    });
    dispatch({ type: "resetInput" });
  };

  const resetItems = () => {
    dispatch({ type: "resetItems" });
  };
  return (
    <div>
      <input type="text" onChange={e => hundleChange(e)} value={input} />
      <button onClick={addItem}>追加</button>
      <button onClick={resetItems}>リセット</button>
    </div>
  );
};

export default InputArea;

もしuseStateを使っている場合は親コンポーネントからコールバック関数を受け取り、発火させることでstateの更新ができましたが、
useReducerを使っている場合は、dispatchを渡し、子コンポーネント内でstate変更のactionを記述すれば良いので、stete更新のためのロジックが親コンポーネントに溜まらないので、見通しがよくなりますし、コンポーネントの役割が明確になります。
また、コンポーネントツリーが大きくなっている場合は、propsでなく、useContextと組み合わせて使えば、小規模アプリならreduxを使わずに、useContextuseReducerでコードの肥大化を防ぐことができるでしょう。

レンダーごとに計算を実行されないように処理をキャッシュしたい

useMomo

useMemoはメモ化されたを返します。
メモ化というのはプログラムの高速化のための最適化する技術の1つで、処理結果を保持しておき、あるトリガーがあるまで処理を行わずに保持してある値を返すことを言います。

下記の例はuseMomeを使っていないコンポーネントで、

  • テキストボックスの値を入力することで、stateのinputが更新
  • ボタンを押すと、stateのcountの三乗が計算され、consoleが出る

というものです

import React, { useState, useMemo } from "react";

const Memo = () => {
  const [count, setCount] = useState(2);
  const [input, setInput] = useState("");

  const newCount = () => {
    console.log("計算します");
    return Math.pow(count, 3);
  };

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <p>{newCount()}</p>
      <button onClick={() => setCount(prev => prev + 1)}>increment</button>
    </div>
  );
};

export default Memo;

上記だとonChangeが発火されるたびに、consoleに計算しますが出力されます。
これは計算結果が変わることはないのにnewCount関数が実行され、無駄な計算を繰り返していることになります。
今回の例は、そこまで複雑な計算ではないですが、もっと複雑になったり、他の処理も重なってくるとレンダリング速度のパフォーマンスの低下に繋がります。

ここでuseMomeを使います

 const newCount = useMemo(() => {
    console.log("render");
    return Math.pow(count, 3);
 }, [count]);

これによりnewCountはメモ化された値の入る変数になり、引数として配列に入っているcountに変更があるまで処理は実行されません。
先ほどの問題は解消され、onChangeが発火して、stateのinputが書き換わっても、再度計算されなくなりました。

useMemoの第二引数の指定に注意

  • 引数なし:全てのstate, propに依存します。結果的に不必要に処理が行われるためメモ化する意味がなくなります。
  • 空配列[]:何にも依存しません。処理が行われるのはレンダー直後のみになります。
  • 値を入れた配列:配列内の変数に変更があるたびに、処理が実行されます。複数入力可能です。useMemoを使う場合は、忘れないようにしましょう。

親からpropsとして渡される関数による無駄なレンダーを避けたい

useCallback

簡単にいうとuseMomeの関数版です。
useCallbackの場合はメモ化された関数を返します。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3