【React】Hooks APIを使用する

この記事ではReact 16.8で正式にリリースされましたHooks APIのうち、よく使われるであろうAPIについてサンプル付き (JavaScript & TypeScript) で紹介していきます。useDebugValueuseImperativeHandleなどのAPIは公式のAPIリファレンスを参照してください。

もくじ

なぜHooks APIが必要だったか (公式Documentから) 🍳

Facebook社のReact.jsの公式ページ (Motivation)には現状のReactにはいくつか下記のような問題点があり、 React Hooks API はその問題点を解消するために実装されたと記述されています。以下、上記の公式ページのMotivationの項の要旨をかいつまんで紹介していきます。

1: React Hooks APIでState関連のロジックを再利用しやすくなり、そのロジック単体を対象としてテストコードを書きやすくなる

Reactではライブラリ単体の機能としてはコンポーネントに特定の"再利用可能"な振る舞いを付与するAPIが提供されていませんでした。そのため、render propshigher-order components などのパターンを使って、これらの問題を解決する (共通の振る舞いを切り出して付与する) 流れでしたが、これらのパターンは、しばしば扱いにくく、読みにくいコードを生みがちでした。(例えば、ChromeのWebインスペクターでReact Developer Toolsを使ったことがあれば、ProviderやConsumerなどの HoCのコンポーネントが幾重にも重なって 無数に表示されていて ("wrapper hell" => ラッパー地獄 ) 、非常に見辛いと思ったことがあると思います。

Hooks APIを使えば、State関連のロジックをコンポーネントから切り離すことができ、コンポーネント間で手軽に機能を再利用して使い回すことができるようになります。またコンポーネントとは独立してテストコードを記述することが可能になり、"wrapper hell" も避けることができるようになります。

2: Hooks APIのリリースで一つの巨大なコンポーネントをそれぞれの関心事に基づいて分割した関数群へと分割でき、コードを把握しやすくできる。

最初はシンプルだったコンポーネントが、State (状態)や、componentDIdMount などのライフサイクルメソッドで氾濫し、最終的には複雑怪奇なものとなって、メンテナンスが難しくなってしまうことがあります。

ライフサイクルのメソッドには通常、それぞれ独自のロジックが記述されます。例えば、componentDidMountcomponentDidUpdate ではAPIからのデータ取得を行うロジックが書かれているとしましょう。でも component DidMount には別に要素に対してイベントリスナーを付与するコードも記述されています。そして、 componentWillUnmount ではその付与したイベントリスナーを削除するコードが書かれているとしましょう。そして、各々のライフサイクルに対して、複雑にStateが絡み合っているようなコンポーネント。

このような、全く関連のない異なったロジック群が一つのコンポーネントに記述してあると分割が一般的に難しいくなります。容易にバグが仕込まれやすい状態になり、メンテナンスも難しくなります。こうしたコンポーネントは一般にStateに関連するロジックが至るところに散らばっているので、そもそも分割することが難しく、テストも難しくなります。Hooksを使うことで一つの巨大なコンポーネントを相互に関連したロジック群に分割することができるようになります。

3: Hooks APIがリリースされることにより、Reactの学習コストが下がる。およびReactが将来的にAoTコンパイル (ahead-of-time compilation) に対応するための道筋をつけられる

先述したとおり、Hooks APIでは Class構文 なしで多様なロジックを定義することができます。コンポーネントの定義にクラスを使うか、関数を使うかはベテランのフロントエンドエンジニアの間でもしばしば議論になりがちです。 また、クラスを使わずにロジックを記述できるようにしたことで、諸々の学習コストを下げることをできる旨が主張されています。

加えて、クラスは将来的にReactがAoTコンパイルを導入しようとしたときにコードの最適化を妨げる要素になる可能性があるとされており、クラスに代わって、積極的にHooks APIの使用を推奨しています。

上記で公式を参照しながら、簡単な要旨について紹介しましたので、早速Hooks APIの使用例をみていきたいと思います。

※ この記事では React v16.8.0 を使用しています。

  "dependencies": {
    "react": "16.8.0",
    "react-dom": "16.8.0"
  },

useState 🍉

useState (Docs)

Hooksの useState を使えば、クラス構文を使わずに、コンポーネントにStateとStateに関する操作ロジックを持たせることができます。

Edit useState
import React, { useState } from "react";
import { render } from "react-dom";

const App = () => {
  const [count, setCount] = useState(0);
  const onClick = () => setCount(count + 1);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onClick}>Increment</button>
    </div>
  );
};

const rootElement = document.getElementById("root");
render(<App />, rootElement);

useState の引数には、初期状態の State を渡します。上記の場合には、「0」という値が 初期状態のステート (State) となります。useStateの戻り値には配列が返ってきます。そして1番目の count という変数がStateの値となり、2番目の setCount という関数が Stateを変更・更新するための関数 となります。上記ではbutton要素にonClickでハンドラーを設定して、buttonがクリックされるたびに、ReactコンポーネントのStateが更新され、setState関数を介して、 値がインクリメントされるように記述しています。

TypeScript であれば、useStateの呼び出し箇所で ジェネリクス(generics) により、 Stateの型 を指定することができます。コンポーネント自体の戻り値の型は React.FC となります。

// TypeScript
import React, { useState } from "react";
import { render } from "react-dom";

const App = (): React.FC => {
  const [count, setCount] = useState<number>(0);
  const onClick = () => setCount(count + 1);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onClick}>Increment</button>
    </div>
  );
};

const rootElement = document.getElementById("root");
render(<App />, rootElement);

useEffect 🍐

useEffect (Docs)

Hooksの useEffect には「副作用を発生させる処理」を記述します。例えば、 タイマー処理外部APIサーバへのデータ取得リクエスト などの 副作用 を発生させる処理 (従来の ライフサイクルメソッド で以前は、 componentDIdMountcomponentWillUnmount に記述していたロジック) などが該当します。こういった処理には、 useEffect を使用します。 useEffect に渡された関数はレンダリングが走った直後に毎回実行されます。もし、レアケースですが、DOM操作をしたい場合 (例えば、マウントされたタイミングでコンポーネントのスタイルを変更したいなど) には、別のuseLayoutEffectを使用しましょう。一瞬Reactコンポーネントがレンダリングされたあとに、Stateが更新されることによって、画面の表示が切り替わること (チラツキ) 等を防ぐことができます。

Edit useEffect1
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";

const Child1 = () => {
  useEffect(() => {
    /* コンポーネントレンダリング後に毎回走る */
    console.log("child1 Mounted");

    /* コンポーネントがアンマウントされたときに走る */
    return () => {
      console.log("child1 unMounted");
    };
  });

  return <span>Child1</span>;
};

const Child2 = ({ count }) => {
  useEffect(() => {
    /* コンポーネントレンダリング後に毎回走る */
    console.log("child2 Mounted");

    /* コンポーネントがアンマウントされたときに走る */
    return () => {
      console.log("child2 unMounted");
    };
  });

  return <span>Child2</span>;
};

const App = () => {
  const [flag, toggle] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          toggle(!flag);
        }}
      >
        Toggle Component
      </button>
      <div>{flag ? <Child1 /> : <Child2 />}</div>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

上記サンプルでは Stateのflagがtrueかfalseかによって、レンダリングするコンポーネントを決定しています。flagがtrueの場合にはChild1コンポーネントを、flagがfalseの場合にはChild2コンポーネントをレンダリングするようにしています。useEffect で表示されるReactコンポーネント内部で useEffect に記述したコールバックがそれぞれ、特定のタイミングで走ります。

パフォーマンスチューニング

基本的に useEffect に記述したコールバックはReactコンポーネントが再レンダリングされるたびに、毎回走ります。しかし、下記のように useEffect の第2引数に配列の形式で値を渡してあげることで、前回と同じ値のときには 再レンダリング時でも実行しないように制御 することができます。パフォーマンス上の要請から毎回レンダリングされるたびに処理を実行したくない場合に有用です。

Edit useEffect2
import React, { useEffect, useState} from "react";
import ReactDOM from "react-dom";

const ColoredNum = ({ count }: { count: number }) => {
  useEffect(
    () => {
      /* 2で割った余りが0のときには再レンダリング時であっても実行されない */
      console.log("executed!");
    },
    [count % 2 === 0]
  );

  console.log("re-rendered");

  return <div>{count}</div>;
};

const App = () => {
  const [count, setCount] = useState(2);

  return (
    <div>
      <input
        type="number"
        value={count}
        onChange={e => {
          setCount(e.currentTarget.value);
        }}
      />
      <ColoredNum count={count} />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useContext 🍓

useContextReact.createContext で生成されたContextオブジェクトを引数に取り、Contextの現在のvalueを取得します。そのときのvalueはContextに対応する至近のProviderから取得されます。

Edit useContext
import React, { useContext, useState } from "react";
import ReactDOM from "react-dom";

const initialState = 10;
const context = React.createContext(null);

const MyGrandChild = () => {
  const myContextVal = useContext(context);

  const increment = () => {
    myContextVal.setState(myContextVal.state + 1);
  };

  return (
    <div>
      <button onClick={increment}>increment</button>
      {myContextVal.state}
    </div>
  );
};

const MyChild = () => (
  <div>
    <MyGrandChild />
  </div>
);

const App = () => {
  const [state, setState] = useState(initialState);

  return (
    <context.Provider value={{ state, setState }}>
      <div>
        <MyChild />
      </div>
    </context.Provider>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useReducer 🍒

useReducer (Docs)

Redux などのState Managerライブラリーを使ったことがあれば、Reducer という概念には馴染み深いと思います。より複雑なStateに絡むロジックを記述する場合には、こちらの useReducer のほうが useSrtate よりも好ましいケースの場合には、こちらのAPIを用います。

Edit useReducer
import React, { useReducer } from "react";
import { render } from "react-dom";


const initialState = {
  todos: []
};

const reducer = (state, action) => {
  switch (action.type) {
    case "reset":
      return initialState;
    case "add":
      return { todos: state.todos.concat(action.payload) };
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "add", payload: "item" })}>
        Add an item
      </button>

      <ul>
        {state.todos.map((todo, index) => (
          <li>
            {index}: {todo}
          </li>
        ))}
      </ul>
    </div>
  );
};

const rootElement = document.getElementById("root");
render(<App />, rootElement);
const [state, dispatch] = useReducer(reducer, initialState, initialAction);

useReducer の第一引数には Reducer関数 をセットし、第二引数には初期状態のStateをセットします。また、オプションで第三引数に初期レンダリング時に発火するアクションを定義することができます。 dispatch関数 が戻り値として戻ってくるので、それを使ってStateを更新します。

useRef 🥝

useRefrefオブジェクト を返します。Reactの従来の Ref と同じようにHTML要素に対する参照を持つことができます。

Edit useRef
import React, { useRef } from "react";
import ReactDOM from "react-dom";

function App() {
  const myBtnRef = useRef(null);
  const onButtonClick = () => {
    myBtnRef.current.style.backgroundColor = "red";
  };

  return (
    <div>
      <button onClick={onButtonClick} ref={myBtnRef}>
        Click ME!
      </button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useMemo 🍊

useMemoメモ化された値を返します。

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

useMemoは引数にとった値 (上記の場合は a, b) が変わったときにのみ、再計算処理を走らせます。重い処理を不必要に再レンダリング時に毎回実行しないことで、パフォーマンス向上用途に使用できます。

※ useMemoに渡した関数はレンダリングの最中に実行されます。 APIへのリクエストなどの副作用処理は useMemoではなく、useEffectに記述 しましょう。メモ化されたコールバックを返すuseCalbackというAPIもあります。

Edit useMemo
import React, { useState, useMemo } from "react";
import ReactDOM from "react-dom";

const App = ({ data }) => {
  const [state, setState] = useState(0);

  const sorted = useMemo(
    () => {
      console.log("メモ化処理が走りました!");
      return data.sort((a, b) => a - b);
    },
    [data]
  );

  console.log("コンポーネントのレンダリング処理が走りました!");

  return (
    <>
      <button
        onClick={() => {
          setState(state + 1);
        }}
      >
        update
      </button>
      <ul>
        {sorted.map((val, idx) => (
          <li key={idx}>{val}</li>
        ))}
      </ul>
    </>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(
  <App
    data={[
      7,
      28,
      82,
      9,
      3,
      2,
      55,
      72,
      8,
      99,
      5,
      43,
      32,
      5,
      2,
      8,
      2,
      5,
      7,
      6,
      0
    ]}
  />,
  rootElement
);

useCallback 🥑

useCallback はメモ化されたコールバックを返します。第二引数に渡した値が変更したかによって、新たに値をメモ化するかを、決定します (パフォーマンス・チューニング)。下記ではcb1はStateの変更に応じて再度メモ化されますが、コールバックcb2は一度メモ化されてしまうと、再度更新はされません。

Edit customHooks
import React, { useCallback, useState } from "react";
import ReactDOM from "react-dom";

const getCallback1 = state =>
  useCallback(
    () => {
      alert(state);
    },
    [state]
  );

const getCallback2 = state =>
  useCallback(() => {
    alert(state);
  }, []);

const App = () => {
  const [state, setState] = useState(1);
  const cb1 = getCallback1(state);
  const cb2 = getCallback2(state);
  const updateState = () => {
    setState(state + 1);
  };

  return (
    <div>
      <h1>State: {state}</h1>
      <button onClick={updateState}>updateState</button>
      <br />
      <button onClick={cb1}>Callback1</button>
      <br />
      <button onClick={cb2}>Callback2</button>
      <br />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

カスタムHooksをつくる 🍄

カスタムHooksを自分で作ってコンポーネントから、ロジックを再利用可能な形で抽出することができます。カスタムHooksの命名には「 use 」と頭に付けます (命名規約)。下記はコンポーネントがマウントされたときに、指定のCSSアニメーションを付与するカスタムHooksです。

頭に「use」とつける以外にも、Hooksには他にも 下記のルールがあります。

  • 1: ※ React Functionの中で呼ばれること*
  • 2: ※ トップレベルで呼ばれること (ループ処理やif文の中、ネストされた関数の中で呼ばれないこと)

下記のサンプルでは動画 (Video) の現在の再生時間をStateとして管理し、それを変更するためのコールバックを返すカスタムHooksを定義しています。

Edit 27q8nnp30
import React, { useState, useMemo, useRef } from "react";
import ReactDOM from "react-dom";

const useVideoCtrl = ref => {
  const [currentTime, setCurrentTime] = useState(0);

  const onPlay = () => {
    console.log("play");
  };
  const onPause = () => {
    console.log("pause");
  };

  const onTimeUpdate = () => {
    setCurrentTime(ref.current.currentTime);
  };

  return {
    onPlay,
    onPause,
    onTimeUpdate,
    currentTime
  };
};

const App = () => {
  const ref = useRef(null);
  const { currentTime, onPlay, onPause, onTimeUpdate } = useVideoCtrl(ref);

  return (
    <div>
      <h1>{currentTime}</h1>
      <video
        onPlay={onPlay}
        onPause={onPause}
        onTimeUpdate={onTimeUpdate}
        ref={ref}
        controls
      >
        <source
          src="xyz.mp4"
          type="video/mp4"
        />
      </video>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

HoCでやっていたようにコンポーネントをラッピングして、新しいコンポーネントを返すような 下記のようなHooksも作れます。(使い方として妥当ではない気もしますが。) コンポーネントがマウントされたときに、アニメーション効果を付与するサンプルです。

Edit customHooks
import React, { useEffect, useRef, useMemo, useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";

/* カスタムHooks */
const useMyStyle = (component, styles) => {
  const [isMounted, setIsMounted] = useState(false);
  const Wrapper = useMemo(
    () => styled.div`
      ${styles.common}
      ${props =>
        props.isMounted ? styles.after : styles.before};
    `,
    []
  );

  useEffect(
    () => {
      setIsMounted(true);
    },
    [true]
  );

  return <Wrapper isMounted={isMounted}>{component}</Wrapper>;
};

const App = () => {
  const styles = {
    common: "transition-duration: .6s;",
    before: "transform: translate(0, 20px); opacity: 0;",
    after: "transform: translate(0, 0);  opacity: 1;"
  };

  return useMyStyle(<span>Hello</span>, styles);
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
  • このエントリーをはてなブックマークに追加