81
80

More than 5 years have passed since last update.

【React】目的別hooks活用法【hooks】

Last updated at Posted at 2019-08-17

2月の頭にReactのv16.8がリリースされ、hooksが正式に使えるようになってから半年が過ぎました。

非常に強力なhooksですが、クラスコンポーネントに慣れた方の中にはhooksを上手く使えないという方も多いのではないかと思います。
そこで今回は、クラスコンポーネントで行っていた様々な処理hooksを用いて関数コンポーネントで実現するための解説を行おうと思います。

本記事は、React hooksを知っていて、API自体はなんとなくわかっていることを前提としています。
もしhooksが全くわからない場合は、公式リファレンスなどを参照ください。

また、本記事に記載しているコードはReact v16.9.0とTypeScript v3.5.31で動作確認をしており、GitHubでのコード公開と、Netlifyでのホスティングをしています。

クラスコンポーネントでやっていたあれこれを置き換える

早速、クラスコンポーネントで行っていた様々な処理をhooksを使って関数コンポーネント(以下FC)に置き換えてみましょう。

componentDidMountでAPIリクエスト

Reactのみでシンプルなアプリケーションを作る場合、componentDidMountでAPIリクエストを行うこともあるかと思います。

そのような処理は、useStateuseEffectを使って以下のように書くことができます。

要件
- コンポーネントマウント時にAPIリクエストを行う
- APIリクエスト中にはローディング表示を行う

ComponentDidMount.tsx
import React, {useEffect, useState} from 'react';

type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};

// APIリクエストのダミー
const request: () => Promise<SomeState> = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve({id: '001', name: 'remew'}), 1000);
  });
};

const ComponentDidMount = () => {
  const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
  useEffect(() => {
    request()
      .then(json => setState({data: json, isLoading: false}));
  }, []);
  if (state.isLoading) {
    return <p>Loading...</p>;
  }
  return (
    <>
      <p>id: {state.data.id}</p>
      <p>name: {state.data.name}</p>
    </>
  );
};

export default ComponentDidMount;

useEffectの第二引数に配列を指定すると、コンポーネントが再レンダリングされた際に配列の中身が変わっていないかを確認してもし変わっていれば第一引数に指定した関数が実行されます。
そのため、空配列を指定すると初回レンダリング時にのみ実行されるため、componentDidMount相当の処理をすることができます。

componentDidMount/componentDidUpdateでAPIリクエスト

マウント時だけではなく、propsの特定の値が変わったときにAPIリクエストをしたいシチュエーションもあると思います。
これも同じくuseStateuseEffectを用いて実現することが可能です。
察しの良い方はわかると思いますが、useEffectの第二引数に監視するpropsの値を渡せば良いです。

ComponentDidMountUpdate.tsx
import React, {useEffect, useState} from 'react';

type MaybeLoading<T> = {data: T, isLoading: false} | {data: null, isLoading: true};
type SomeState = {id: string, name: string};

// APIリクエストのダミー
const request: (id: string) => Promise<SomeState> = (id) => {
  return new Promise(resolve => {
    setTimeout(() => resolve({id, name: 'remew' + id}), 1000);
  });
};

// id指定用のラッパー
const Wrapper = () => {
  const [id, setId] = useState('');
  return (
    <>
      <label>id:<input onInput={(e: any) => setId(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <ComponentDidMountUpdate id={id} />
    </>
  );
};

const ComponentDidMountUpdate: React.FC<{id: string}> = props => {
  const [state, setState] = useState<MaybeLoading<SomeState>>({data: null, isLoading: true});
  useEffect(() => {
    if (!props.id) {
      return;
    }
    request(props.id)
      .then(json => setState({data: json, isLoading: false}));
  }, [props.id]);
  if (!props.id) {
    return <p>id is empty.</p>;
  }
  if (state.isLoading) {
    return <p>Loading...</p>;
  }
  return (
    <>
      <p>id: {state.data.id}</p>
      <p>name: {state.data.name}</p>
    </>
  );
};

export default Wrapper;

componentDidMountcomponentWillUnmountで処理を行う

例えば、1秒ごとに数値が加算されるようなコンポーネントを考えてみましょう。

クラスコンポーネントであれば、componentDidMountcomponentWillUnmountを組み合わせて実現することが可能だと思います。
対してFCでは、useEffectを用いて以下のようなコードで実現することができます。

TimerCountFC.js
import React, {useEffect, useState} from 'react';

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

  useEffect(() => {
    const timerId = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);

    return () => {
      console.log('clear interval'); // 確認用
      clearInterval(timerId);
    };
  }, []);

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

export default Timer;

useEffectの第二引数に空配列を指定することで、componentDidMount相当の処理を行っています。
その中でsetIntervalを呼び出し、更にその中ではsetCountを呼び出しています。

useEffectの第一引数に渡した関数の返り値として返している関数は、コンポーネントのアンマウント時と、第一引数に渡した関数が再実行される直前に実行されます。
今回はコンポーネントがアンマウントされるときのみ実行されますが、第二引数に何らかの値を渡している場合、その値が変わって関数が再実行される際にも実行されます。

また、なぜsetCountにアロー関数を渡しているのかわからない方もいるかもしれませんが、本記事では深くは解説しません。
簡単に言うと、setIntervalに渡した関数から見えるcount変数は初回レンダリング時の値(今回は初期値0)であるため、setCount(count + 1)としても毎回1がセットされてしまうためです。
詳しい説明は、React公式が提供しているQ&Aをご覧下さい。

Contextを使用する

ReactのContext APIは、複数のContextをクラスコンポーネントで使用する際は、render propsパターンを用いる必要がありました。

useContextというhooksを用いることで、非常にシンプルにContextの値を使用することができます。

UseContext.tsx
import React, {useContext, useState} from 'react';

const colorContext = React.createContext<string>('#000');

const Wrapper = () => {
  const [color, setColor] = useState('#000');
  return (
    <>
      <input value={color} type={'color'} onChange={e => setColor(e.target.value)} />
      <colorContext.Provider value={color}>
        <UseContext />
      </colorContext.Provider>
    </>
  );
};

const UseContext = () => {
  const color = useContext(colorContext);

  return <p style={{color}}>{color}</p>;
};

export default Wrapper;

SomeContext.Providerで値を渡す部分は変わりませんが、<SomeContext.Consumer>{value => ...}</SomeContext.Consumer>とする必要があった部分が非常にシンプルになっていることがわかると思います。

PureComponent/shouldComponentUpdate

React製アプリケーションのパフォーマンスチューニングの手段として、shouldComponentUpdateを使用したり、React.PureComponentを継承している人も多いと思います。

FCでもpropsが変化しない場合の再レンダリングを抑制するためには、React.memoを使用する必要があります。
これはhooksではありませんが、FCを利用する際に必要な知識のためついでに説明しておきます。

ReactMemo.tsx
import React, {useContext, useEffect, useState} from 'react';

const SomeComponent: React.FC<{id: string, name: string}> = props => {
  console.log('re-rendering:', props.id);
  if (!props.name) {
    return <div>name is empty</div>;
  }
  return <div>name: {props.name}</div>;
};

const MemoizedSomeComponent = React.memo(SomeComponent);

const HighFrequencyUpdateComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count => count + 1);
    }, 100);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <p>count: {count}</p>
      <SomeComponent id={'not-memoized'} name={name} />
      <MemoizedSomeComponent id={'memoized'} name={name} />
    </>
  );
};

export default HighFrequencyUpdateComponent;

上記コンポーネントを表示すると、コンソールにre-rendering: not-memoizedという表示が連続して表示されますが、re-rendering: memoizedは1度しか表示されないはずです(nameのinputを変更すると再表示されます)。

React.memoに渡したコンポーネントはPureComponentを継承したコンポーネントのように、propsが変更されたときにのみ再レンダリングが実行されるようになります。

また、React.memoには第二引数として関数を渡すことができ、その関数がtrueを返した際にはレンダリングがスキップされます(shouldComponentUpdateと逆なので注意)。

ReactMemoWithComparator.tsx
import React, {useContext, useEffect, useState} from 'react';

const SomeComponent: React.FC<{obj: {id: string, name: string}}> = props => {
  console.log('re-rendering:', props.obj.id);
  if (!props.obj.name) {
    return <div>name is empty</div>;
  }
  return <div>name: {props.obj.name}</div>;
};
const WrongMemoizedSomeComponent = React.memo(SomeComponent);
const MemoizedSomeComponent = React.memo(SomeComponent, (oldProps, newProps) => {
  return oldProps.obj.id === newProps.obj.id && oldProps.obj.name === newProps.obj.name;
});

const HighFrequencyUpdateComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count => count + 1);
    }, 500);
    return () => clearInterval(intervalId);
  }, []);

  return (
    <>
      <label>name:<input onInput={(e: any) => setName(e.target.value)} style={{border: 'solid 1px #000'}} /></label>
      <p>count: {count}</p>
      <SomeComponent obj={{id: 'not-memoized', name}} />
      <WrongMemoizedSomeComponent obj={{id: 'wrong-memoized', name}} />
      <MemoizedSomeComponent obj={{id: 'memoized', name}} />
    </>
  );
};

export default HighFrequencyUpdateComponent;

先程の例と違い、propsの型を{obj: {id: string, name: string}}というように階層構造にしています。

WrongMemoizedSomeComponentは、単純にReact.memoでラップしているだけですが、props.objは毎回違うオブジェクトの参照になっているためレンダリングの抑制をすることができていません。

MemoizedSomeComponentではReact.memoの第二引数に比較関数を渡しています。
props.obj.idprops.obj.nameをしっかり比較しているため、レンダリングの抑制ができています。

さいごに

以上で目的別hooksの活用法の紹介を終わります。

他にも思いついたら追記したいと思います。

hooksが登場した当初は「関数コンポーネントに状態が持てる!」「末端の関数コンポーネントで状態が必要になったときに修正が少なくて済む」などのような意見が多かったように思います。

しかし個人的には、hooks関数コンポーネントに状態を持たせるためだけに使うだけではなく、クラスコンポーネントを関数コンポーネントに置き換えるために使うことでより真価を発揮する機能だと思っています(あくまで新規コンポーネントを作成するときや、既存コンポーネントを修正するときの話です2)。

hooksを駆使してはじめから関数コンポーネントベースで開発をすることで、開発効率が向上するのではないかと考えています。

みなさんが良いhooks生活を送れることを祈っています。

参考リンク


  1. TypeScriptを使っているのは、TypeScriptと絡めた説明もするつもりだったためです。 

  2. Reactの公式ドキュメントではクラスコンポーネントを全部書き換える必要はないと言われており、既存のクラスをフックに書き換えることも推奨されていません。 

81
80
1

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
81
80