2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[React]フックを使ってより扱いやすいコンポーネントにする

Posted at

はじめに

フックとは何か知っていますでしょうか?2020年に追加された新機能で、今までクラスコンポーネントでしか出来なかったことが関数コンポーネントでも出来るようになったものです。今はまだ開発で必須というまではいかないかもしれないですが、今後はポピュラーになっていくと思うので、今のうちに理解しておくと良いでしょう!

React Hooksとは

Reactのバージョン16.8.0で追加された機能で、関数コンポーネントで利用できる関数のことです。これまで、クラスコンポーネントでしか利用できなかったstateなどの機能が関数コンポーネントでも利用できるようになったので、より理解しやすいコードを書けるようになりました。

特徴

  • ステートを持ったロジックを、コンポーネントの階層構造を変えることなしに再利用できる

    • テストもしやすくなる。
  • 関連する機能に基づき、1つのコンポーネントを複数の小さな関数に分割することが可能

  • より多くのReactの機能をクラスを使わずに利用できる

    • クラスコンポーネントに比べ、コード量も減らせる
  • thisを使わずに済むため他の開発者が見ても理解しやすい

使用時のルール

  • フックを呼び出すのはトップレベルのみ

    • フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけない
    • return 文よりも前の場所で呼び出すことで、コンポーネントがレンダーされる際に毎回同じ順番で呼び出される
  • フックを呼び出すのはReactの関数内のみ

    • 通常のJavaScript関数から呼び出してはいけない
    • コンポーネント内のすべてのstateを使うロジックがソースコードから参照可能になる

React Hooksの使い方

ここから、よく使うフックや、自分でフックを作成するやり方について見ていきます。

useState

useStateとは、関数コンポーネントでstateを管理(stateの保持と更新)するためフックです。
※おそらく最も利用されるフックかと思います。

const [state, state更新関数] = useState(初期値)のように書きます。

// ここで使用するフックをインポートしておく
import React, { useState } from 'react';

function Example() {
  // 初期値が0のcountと更新する関数を定義
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      { // クリックされると、上で定義したsetCountを呼び出してstateを更新する }
      <button onClick={() => setCount(count + 1)}>
        Click!
      </button>
    </div>
  );
}

useEffect

useEffectとは、コンポーネントに副作用を追加するフックです。これを利用することでコンポーネントのrender後もしくはアンマウント後に副作用(渡された関数)を実行できます。
※クラスコンポーネントで言うライフサイクルメソッドのことです。

ちなみに副作用とは以下のようなことを言います。

  • Reactから生成されたDOMの変更
  • APIとの通信
  • 非同期処理
  • console.log

useEffect(副作用, 依存配列(副作用が依存している値が格納された配列))のように書きます。Reactコンポーネントにおける副作用には2種類ありますので、1つずつ確認していきます。

クリーンアップを必要としない副作用

ReactがDOMを更新した後で追加のコードを実行したいという場合があります。ネットワークリクエストの送信、手動での DOM 改変、ログの記録、といったものがクリーンアップを必要としない副作用の例です。

// ここで使用するフックをインポートしておく
import React, { useState, useEffect } from 'react';

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

  // render後もしくはアンマウント後に実行する
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

ちなみに、クラスで書くと以下のようになるためコードが冗長な事がわかります。
(2回同じコードを書かなければならない)

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click!
        </button>
      </div>
    );
  }
}

クリーンアップを有する副作用

開発をしていく中で、イベントリスナの削除やタイマーのキャンセルなどクリーンアップしたい場面に遭遇する事があります。クリーンアップ関数を返すことで、2度目以降のrender時に前回の副作用を消してしまうことができます。
以下の例はタイマーを

// ここで使用するフックをインポートしておく
import React, { useState, useEffect } from 'react';

// タイマーの限界値を設定
const LIMIT = 60;

function Timer() {
  const [timer, setTimer] = useState(LIMIT);

  // timeをリセットするメソッドを定義
  const reset = () => {
    setTimer(LIMIT);
  };

  // timeを更新するメソッドを定義
  const tick = () => {
    setTimer((prevTimer) => (prevTimer === 0 ? LIMIT : prevTimer - 1));
  };

  useEffect(() => {
    // 1秒毎にtickを実行するタイマーを作成
    const timerId = setIntarval(tick, 1000);
    // タイマーをリセットする関数を返すことでクリーンアップされる
    return () => {
      clearInterval(timerId);
    };
  }, []);

  return (
    <div>
      <p>time: {timer}</p>
      <button onClick={reset}>RESET!</button>
    </div>
  );
}

### 副作用のスキップによるパフォーマンス改善
デフォルトでは、副作用関数は初回のレンダー時および毎回の更新時に呼び出されてしまいます。そのため、パフォーマンスの問題を引き起こす可能性があります。
そこで、配列を渡してあげることでこの問題を解決出来ます。

依存配列を渡す

第二引数に渡した依存配列が更新されたタイミングで、実行させる事ができます。

...
// valueが更新されたタイミングで実行される
useEffect(() => {
  console.log(value);
}, [value]);
...

空の配列を渡す

第二引数にからの配列を渡すことで、renderされた後に1度だけ実行させる事ができます。

...
// render後に1度だけ実行される
useEffect(() => {
  console.log('render終了');
}, []);
...

useRef

useRefとは、refオブジェクトを返すフックのことです。
refオブジェクトを利用することで、DOMの参照やコンポーネント内で値を保持出来ます。useStateとは異なり、生成した値を更新しても再renderされることはないです。
そのため、コンポーネント内で値を保持したいが、値を更新した際にrenderされたくない時に使用します。

const refオブジェクト = useRef(初期値);のように書きます。

基本的な使い方

// ここで使用するフックをインポートしておく
import React, { useRef } from 'react'
...
// 初期値で渡した値が、refオブジェクトのcurrentプロパティの値となります
const num = useRef(10);
console.log(num.current); // => 10

// 渡した値に1を足す(更新処理)
num.current = num.current + 1

DOMを参照したい場合

// ここで使用するフックをインポートしておく
import React, { useRef } from 'react'
...
const inputEl = useRef(null);
...
<input ref={inputEl} type='text'/>
...
console.log(inputElement.current); // => <input type="text/>が取得できる

useCallback

useCallbackとは、メモ化されたコールバック関数を返すフックのことです。メモ化したコンポーネントにuseCallbackでメモ化したコールバック関数をPropsとして渡すことで、コンポーネントの不要な再renderをスキップする事ができます。

useCallback(コールバック関数, 依存配列);のように書きます。

// ここで使用するフックをインポートしておく
import React, { useState, useCallback } from 'react';

const Child = React.memo(({ handleClick }) => {
  console.log('render Child');
  return <button onClick={handleClick}>Child</button>;
}

export default function App() {
  const [count, setCount] = useState(0);

  // 関数をメモ化することで、新しいhandleClickと前回のhandleClickは等価となり再renderされない
  const handleClick = useCallback(() => {
    console.log('click');
  }, []);

  return (
    <div>
    <p>Counter: {count}</p>
    <button onClick={() => setCount(count + 1)}>Count!</button>
    <Child handleClick={handleClick} />
    </div>
  );
}

注意点

以下のような場合は再renderされてしまうので注意が必要です。

  • React.memoでメモ化していないコンポーネントにuseCallbackでメモ化をしたコールバック関数を渡したとき

  • useCallbackでメモ化したコールバック関数を、それを生成したコンポーネント自身で利用したとき

useMemo

useMemoとは、メモ化された値を返すフックのことです。コンポーネントの再render時に値を再利用出来るため、値の不要な再計算をスキップする事ができます。
useCallbackとは少し違うのですが、useCallbackは関数自体をメモ化しますが、useMemoは関数の結果を保持します。

useMemo(() => 値を計算するロジック, 依存配列のように書きます。

// ここで使用するフックをインポートしておく
import React, { useState, useMemo } from 'react';

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // わざとかなり時間がかかる処理をしてから2倍する関数を定義
  const double = count => {
    let i = 0;
    while(i < 100000000) i++;
    return count * 2;
  }

  // count2を2倍にした値をメモ化する(count2が更新されたタイミングのみ再計算する
  const doubledCount = useMemo(() => double(count2), [count2]);

  return (
    <div>
      <h2>Increment(fast)</h2>
      <p>Counter: {count1}</p>
      <button onClick={() => setCount1(count1 + 1)}>Increment(fast)</button>
     </div>

    <div>
      <h2>Increment(fast)</h2>
      { // メモ化しているため、count1を更新した時の再renderが高速になる }
      <p>Counter: {count2}, {doubledCount}</p>
      <button onClick={() => setCount2(count2 + 1)}>Increment(fast)</button>
     </div>
  );
}

useContext

useContextとは、Contextオブジェクトから値を取得するフックのことです。こちらは、Providerから共有される値を取得するために必要です。

const Contextオブジェクトの値 = useContext(Contextオブジェクト)のように利用します。

// ここで使用するフックをインポートしておく
import React, { useState, useContext, createContext } from 'react'

// Contextオブジェクトを生成する
const MyContext = createContext();

export default function App() {
  const [count, setCount] = useState(0);
  const value = {
    name: 'React',
    handleClick: () => setCount((count) => count +1)
  }

  return (
    <div>
      <p>count: [count]</p>
      <MyContext.Provider value={value}>
        { // Providerでラップされているのでvalueプロパティの値を取得できる }
        <Child />
      <MyContext.Provider>
    </div>
  );
}

function Child() {
  // Appの孫コンポーネントを返す
  return <GrandChild />;
}

{ // Childの子コンポーネントであることからProviderから値を取得できる }
function GlandChild() {
  const context = useContext(MyContext);

  return (
    <div>
      <p>{context.name}</p>
      <button onClick={context.handleClick}>Increment</button>
    </div>
  );
}

再renderを防ぐために

パフォーマンスの問題の観点から以下の対策を施して、再renderをを防ぎましょう。

  • Contextを分割する

  • React.memoを利用する

  • useMemoを利用する

使い所

  • 複数コンポーネントで共通利用したいデータがあるが、コンポーネントの数が多かったり、階層が深いのでPropsで渡すのが困難もしくはわかりづらい場合
  • Prop drilling問題を解消したいが、良い手段が見つからない場合

デメリット

  • コンポーネントがContextに依存するため、コンポーネントの再利用性が低下する
  • Contextオブジェクトの値がグローバルなstateなので、何も考えずに利用するとかえってわかりづらくなる

useReducer

useReducerとは、'state'と'dispatch(actionを送信する関数)`を返すフックのことです。

const [state, dispatch] = useReducer(reducer,'stateの初期値');のように書きます。

  • reducer

    • stateとactionを受け取り、更新したstateを返す関数
  • dispatch

    • dispatch(action)でreducerを実行する
    • actionは、typeプロパティ(actionの識別子)と値のプロパティで構成する
// ここで使用するフックをインポートしておく
import React, { useReducer } from 'react'

// stateとactionを受け取るreducerを定義する
function reducer(state, action) {
  // typeプロパティによって返す値を判断する
  switch(action.type) {
    case 'INCREMENT';
      return {count: state.count + 1};
    case 'DECREMENT';
      return {count: state.count - 1};
    case 'RESET';
      return {count: 0};
    default;
      return state;
  }
}

esport default function App() {
  // 第二引数に渡した値をstateにセットする
  const [state, dispatch] = useReducer(reducer, {count: 0});

  return (
    <div>
      <p>count: {state.count}</p>
      { // dispatchに渡すtypeプロパティでプロパティで返ってくる値を変えている }
      <button onClick={() => dispatch({type: 'INCREMENT'})}>+<button/>
      <button onClick={() => dispatch({type: 'DECREMENT'})}>-<button/>
      <button onClick={() => dispatch({type: 'RESET'})}>RESET!<button/>
    </div>
  )
}

useStateとの比較表

useState useReducer
ローカルorグローバル ローカル グローバル useContext()と一緒に取り扱う
扱えるstate 数値、文字列、論理値 オブジェクト、配列
stateの取り扱い × 複数を同時に取り扱うことが出来る

使い所

複雑なstateを扱う時に利用すると良いです。ちなみに複雑なstateとは、、

  • stateを更新するために、別のstateを参照する必要がある場合
  • あるstateを更新すると同時に別のstateも更新しなければいけない場合
  • stateを更新するロジック自体が複雑な場合

などのことを指します。

useReducerを用いることで、更新ロジックがuseReducerに集約されて見通しの良いコードとなります。

カスタムフック

カスタムフックとは、自分独自に作成した、コンポーネントからロジックを抽出して再利用可能な関数のことです。カスタムフックを利用することで、以下のようなメリットがあります。

  • コンポーネント内のロジックをカスタムフックとして切り出すことで、複数のコンポーネントで再利用できる
  • 複雑なロジックをカスタムフックとして切り出すことで、コンポーネントの見通しが良くなる
// use〇〇という命名で定義する
const useHello = () => {
  return 'Hello';
};

公開されているカスタムフックを使う

以下のようなサイトに他の人が描いたカスタムフックが公開されており、利用できるため、興味がある人は覗いてみてください!

終わりに

ここまでフックの魅力や使い方を見てきましたが、クラスコンポーネントはいらないんじゃないか?と思った人もいるかもしれません。しかし、以下にも有るようにクラスコンポーネントの使用が非推奨になるわけでは無いので、そちらを使っても構いません。

クラスコンポーネントのユースケースをすべてフックがカバーできるようにする予定ではいますが、クラスコンポーネントのサポートも予見可能な将来にわたって続けていきます。Facebook では何万というコンポーネントがクラスとして書かれており、それらを書き換える予定は全くありません。代わりに、クラスと併用しながら新しいコードでフックを使っていく予定でいます。
(引用: フックの導入 - React)

参考

[React]
(https://ja.reactjs.org/)

[React.memoでメモ化する技術]
(https://qiita.com/ren0826jam/items/844810abc547e96cba41)

[[React]Contextという仕組みについて]
(https://qiita.com/ren0826jam/items/2f6f9471ccc985962fe3)

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?