LoginSignup
8
9

More than 1 year has passed since last update.

これからReact触るなら確実に抑えておきたいuseOO

Last updated at Posted at 2022-12-05

はじめに

開発インターンをしています川端(@haru1125632)です。
PharmaXのアドベントカレンダーの6日目を担当します。
PharmaXではフロントはNext.js(Reactのフレームワーク)を用いて作業しています
そこで今回はReactにまつわる記事を書きました。

概要

Reactにはさまざまなuse〇〇が用意されています。
ただし中には利用頻度の低いものからすごく利用するものまであります。
なので今回は以下の一覧から個人的によく使うもの3つに絞って詳しく見ていきたいと思います。
下記の一覧で斜線をひいているものは今回の対象からは除きます。

※ React上級者のかたはクイズだけやってみてください。もし、できなければ記事を読んでください。

ReactHook(use〇〇)一覧 (以下ではReactHookと呼びます)
*中にはRFCのものも含みます

  • useCallback
  • useContext
  • useDebugValue
  • useDeferredValue
  • useEffect
  • useEvent
  • useId
  • useImperativeHandle
  • useInsertionEffect
  • useLayoutEffect
  • useMemo
  • useReducer
  • useRef
  • useState
  • useSyncExternalStore
  • useTransition

React Hookを使う上での共通ルール

Hookはcomponentのトップレベルまたは独自のフックでのみ呼び出すことができます。

つまり以下のCodeはトップレベルでなく関数内で呼び出しているためErrorになります

import React from 'react';
import { useState } from 'react';
export function App(props) {

  const handleHook = () => {
    const [isShow, setIsShow] = useState(false);
  };
  return (
    <div className='App'>
      <button onClick={handleHook}>押す</button>
    </div>
  );
}

Error

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.

useState

1番有名なReactHookから紹介します

useState is a React Hook that lets you add a state variable to your component.

どういった時必要なのか

通常のlocal変数では不十分なケース

import React from 'react';

export function App(props) {
  let count = 0;

  const onClickPlusOne = () => {
    count = count + 1;
    console.log(count)
  };

  return (
    <div className='App'>
      <h1>{count}</h1>
      <button onClick={onClickPlusOne}>+1</button>
    </div>
  );
}

上記は変数countを表示して、ボタンを押すとcountが1ずつ足されていき画面で表示されているcountが1ずつ増えていくことを期待したCodeです。
これは正しく動作しません.しかし、consoleを見るとbuttonを押すたびにcountの数は増加していることがわかります。

なぜ動作しないのでしょうか?

  1. ローカル変数が変更されても再レンダリングされることはありません。つまり、ローカル変数の変更は再レンダリングのトリガーになりません。
  2. Reactコンポーネントは2回目レンダリングする際はそのcomponentの最初から読み込み直しますが、ローカル変数の変更は考慮していません

上記のcodeをうまく動作させるには次のようなものが必要です。

  1. レンダリング後も状態を必ず保持してくれるもの
  2. 再レンダリングをトリガーするようなもの

これらを実現してくれるのがuseStateなのです。

さっきのCodeをuseStateを用いて書き換えるとこうなります。

import React from 'react';
import { useState } from 'react';

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

  const onClickPlusOne = () => {
    setCount(count + 1);
  };

  return (
    <div className='App'>
      <h1>{count}</h1>
      <button onClick={onClickPlusOne}>+1</button>
    </div>
  );
}

以下では+ボタンを押すたびにcountの値が変化します。

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

countは状態変数で、setCountはsetter関数です。

  1. rendering後も保存した値を保つ状態変数 count
  2. Reactをトリガーしてコンポーネントを再レンダリングさせるsetter関数 setCount

また唯一の引数である0は状態変数countの初期値です。

実際にはどのように行われているかを次に示します。

① 引数の初期値である0を状態変数の0に代入します

② count(0)を画面上に表示します

③ Userがボタンをクリックします。

④ countに0を読み取り、1を追加します

⑤ コンポーネントを再レンダリングします

スクリーンショット 2022-11-17 9.11.13.png


クイズ

このような手順がわかっていれば次のconsoleに表示されるものがわかるはずです。

import React from 'react';
import {useState} from 'react'

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

  const onClickPlusOne = () => {
    setCount(count + 1);
    // Q. このlogには何が出力されるでしょう?
    // 1. 0
    // 2. 1
    console.log(count) 
  };

  // Q2. button押した後このlogには何が出力されるでしょう
  // 1. Toplevel: 0
  // 2. Toplevel: 1
  console.log(`Toplevel: ${count}`);

  return (
    <div className='App'>
      <h1>{count}</h1>
      <button onClick={onClickPlusOne}>+1</button>
    </div>
  );
}
正解をみる 正解は
Q1. 0
Q2. Toplevel: 1

set関数を呼び出しても、既に実行中のコードの現在の状態は変更されません。

補足
一度だけ変更されるflagのようなものはuseReducerを使う方が最適のようです。


useEffect

useEffectコンポーネントを外部システムと同期できる React Hook です。

ここでいう外部システムとはブラウザ-API, ネットワークや サードパーティーライブラリのことです。
Effectを使用すると、特定のイベント(Click Hoberなど..)ではなくレンダリング自体によって引き起こされる副作用を指定できます。

useEffectを使用する際は、本当にuseEffectが必要かどうかを検討する必要があります。

In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some action—for example, when you click a button.

通常副作用はEventHandlerに属しています。イベントハンドラーとはボタンをクリックした際に、用意されているonClickなどのことです。

If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort.

それらのイベントハンドラーを全て調査して、どのようにしてもうまくいかなかった場合のみ useEffectを使ってください。 useEffectは最終手段です。

なぜuseEffectが必要なのか

今回は以下のようなことを実現します

  • コンポーネントが最初にレンダリングされた際にAPIを叩く
  • 叩かれたAPIのdataを使って、画面にresponseの配列の1番目のUserの情報を表示する

まず、useEffectを使わないで書いた例を紹介します

import React from 'react';
import { useState, useEffect } from 'react';
const API_URL = 'https://jsonplaceholder.typicode.com/users';

export function App(props) {
  const [user, setUser] = useState({});

  fetch(API_URL)
    .then(res => res.json())
    .then(data => setUser(data[0]))

  return (
    <div className='App'>
      <ul>
        <li>{user.name}</li>
        <li>{user.email}</li>
      </ul>
    </div>
  );
}

何が問題なのか

dataを表示でき、問題のないように見えます。しかし、APP関数内にAPIを叩くCodeが無限に叩かれていることに気づきます。(console.logなどを書いて実際に試してください)

componentは純粋です。レンダリングに使用するsetup関数をトップレベルで使用しないでください。

A component must be pure, You should not mutate any of the inputs that your components use for rendering. That includes props, state, and context. To update the screen, “set” state instead of mutating preexisting objects.

次に useEffectを使用してAPIを叩いた例を紹介します。

import React from 'react';
import { useState, useEffect } from 'react';
const API_URL = 'https://jsonplaceholder.typicode.com/users';

export function App(props) {
  const [user, setUser] = useState({});

  useEffect(() => {
    fetch(API_URL)
      .then(res => res.json())
      .then(data => setUser(data[0]));
  }, []);

  return (
    <div className='App'>
      <ul>
        <li>{user.name}</li>
        <li>{user.email}</li>
      </ul>
    </div>
  );
}

useEffectは二つの引数を受け取ります。

useEffect(setup, dependencies?)

第一引数はそのシステムに接続するsetup関数を渡します、これは特定のイベント(Click Hoberなど..)ではなくレンダリング自体によって引き起こされる副作用によってどのような挙動を起こしてほしいかをこの関数に書きます。

第二引数は依存配列を指定します。これを指定するとどの要素の変更によってレンダリングした際に発火するかを指定できます。指定しない場合は全てのレンダリングによって発火します。また、空文字を指定した際は最初の1回のレンダリングのみ発火します。


クイズ

Q1. 次のconsole.log('HelloWorld)はいつ実行されるでしょうか?

  1. 最初のレンダリング時のみ
  2. ボタンを押した時のみ
  3. 最初のレンダリング時とボタンを押した時
  4. 実行されない
import React from 'react';
import { useState, useEffect } from 'react';

export function App(props) {
  const [count, setCount] = useState(0)
  
  useEffect(()=>{
    console.log('HelloWorld')
  },[count, setCount])

  return (
    <div className='App'>
      <button onClick={()=> setCount((prev) => prev + 1)}>押す</button>
    </div>
  );
}
正解をみる 正解は 3. 最初のレンダリング時とボタンを押した時

Q2. 次のconsole.log('GoodNightWorld)はいつ実行されるでしょうか?

import React from 'react';
import { useState, useEffect } from 'react';

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

  useEffect(()=>{
    
    return () => console.log('GoodNightWorld')   
  },[count, setCount])

  return (
    <div className='App'>
      <button onClick={()=> setCount((prev) => prev + 1)}>押す</button>
    </div>
  );
}
  1. 最初のレンダリング時のみ
  2. ボタンを押した時のみ
  3. 最初のレンダリング時とボタンを押した時
  4. このコンポーネントがアンマウントされたタイミングのみ
  5. このコンポーネントがアンマウントされたタイミングとボタンを押した時
正解をみる 正解は 5. このコンポーネントがアンマウントされたタイミングとボタンを押した時です。

useEffectの第一引数としてcallback関数を指定することでその依存関係の逆が実行された際に発火するようなものを作ることができます。

下記は外部のAPIにコネクションをComponent表示時(mount時)に行い、Componentから離れた場合(unmount時)にコネクションを切断しています.

useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);


useCallback

useCallback is a React Hook that lets you cache a function definition between re-renders.

useCallbackは再レンダリング時に関数定義をキャッシュできるものです

Reactが再レンダリングされる条件

  1. コンポーネントのstateを更新(useStateのset関数を呼び出した時)
  2. 親のコンポーネントが再レンダリングした時
import React from 'react';
import { useState } from 'react';

const Children = () => {
  console.log('childrenさんだよ');
  return <div>I am Children</div>;
};

export function App(props) {
  const [text, setText] = useState('');

  return (
    <div className='App'>
      <input value={text} onChange={e => setText(e.target.value)} type='text' />
      <Children />
    </div>
  );
}

上記のCodeではinputに値が入るたびにChildren Componentのトップレベルで書かれているconsole.log が実行されています。

親コンポーネントの値の変更(今回だとinput要素の変更)は、子コンポーネントを表示する箇所に直接影響ないのに、再レンダリングされてしまっています。 reactではこのような不要なレンダリングを防ぐためにいくつか便利なReact HookやAPIが用意されています。 useMemo useCallback memo
今回の例だと子コンポーネントをmemoで囲むことによってレンダリングを回避することができます。

const Children = memo(() => {
  console.log('childrenさんだよ');
  return <div>I am Children</div>;
});

memoだけでは再レンダリングを防げないケース

memoだけでは不要なレンダリングを防げないケースがあります。
以下に例を示します。簡単な2倍, 3倍カウンターを表現したCodeです。
buttonをコンポーネント化し、onClick時に動作してほしい関数をそれぞれ渡しています。

import React from 'react';
import {useState} from 'react'

const Button = React.memo(props => {
    const {label, onClick} = props;
    console.log('Button Componentがレンダリングされたよ')
    return <button onClick={onClick}>{label}</button>;
});

export const App = (props) => {
    // 2からスタートして2の倍数を管理するstate
    const [multipleTwo, setMultipleTwo] = useState(2);
    // 3からスタートして3の倍数を管理するstate
    const [multipleThree, setMultipleThree] = useState(3);

    // 2倍をセットする
    const clickMultipleTwo = () => {
        setMultipleTwo(multipleTwo * 2);
    };

    // 3倍をセットする
    const clickMultipleThree = () => {
        setMultipleThree(multipleThree * 3);
    };

    return (
        <div className='App'>
            <div>2の倍数: {multipleTwo}</div>
            <Button label='2倍' onClick={clickMultipleTwo}/>

            <div>3の倍数: {multipleThree}</div>
            <Button label='3倍' onClick={clickMultipleThree}/>
        </div>
    );
}

export default App

初回レンダリング時は以下のようにconsoleに2回表示されるはずです、これは初期のレンダリングなので正常な回数分レンダリングされています。

Button Componentがレンダリングされたよ
Button Componentがレンダリングされたよ

補足:
もし、4回レンダリングされてしまった方はstrictModeをoffにしてください。

次に2倍のボタンを押して見ると

Button Componentがレンダリングされたよ
Button Componentがレンダリングされたよ

このように2回レンダリングが走ってしまうことがわかります。

         <Button label='2倍' onClick={clickMultipleTwo}/>

本来はこの上記の部分が再レンダリングされるだけで良いはずです。

        <Button label='3倍' onClick={clickMultipleThree}/>

しかし、実際にはこの部分も再レンダリングされていることがわかります。 

なぜこのようなことが起こったのか

スクリーンショット 2022-11-20 15.23.18.png

1. clickされたので関数が実行される

2. set関数によってApp(親)Componentが再レンダリングされる

3. 再定義された二つの関数がButton Componentに渡される。Button ComponentではPropsの変更を検知して再レンダリングする。

つまり、今回は全く関係のなかったclickMultipleThreeまで再定義されてしまったので子コンポーネントが再レンダリングされてしまったということです。

useCallbackを使って再定義をキャッシュする

import React, {useCallback} from 'react';
import {useState} from 'react'

const Button = React.memo(props => {
    const {label, onClick} = props;
    console.log('Button Componentがレンダリングされたよ')
    return <button onClick={onClick}>{label}</button>;
});

export const App = (props) => {
    // 2からスタートして2の倍数を管理するstate
    const [multipleTwo, setMultipleTwo] = useState(2);
    // 3からスタートして3の倍数を管理するstate
    const [multipleThree, setMultipleThree] = useState(3);

    // 2倍をセットする
    const clickMultipleTwo = useCallback(() => {
        setMultipleTwo(multipleTwo * 2);
    }, [multipleTwo])

    // 3倍をセットする
    const clickMultipleThree = useCallback(() => {
        setMultipleThree(multipleThree * 3);
    }, [multipleThree]);

    return (
        <div className='App'>
            <div>2の倍数: {multipleTwo}</div>
            <Button label='2倍' onClick={clickMultipleTwo}/>

            <div>3の倍数: {multipleThree}</div>
            <Button label='3倍' onClick={clickMultipleThree}/>
        </div>
    );
}

export default App

これによって2倍のボタンを押した際は2倍のボタンのみが再レンダリングの対象になるようになりました👏

const cachedFn = useCallback(fn, dependencies)

useCallbackは第一引数でwrapしたい関数を第二引数でuseEffectと同様依存関係を配列で渡します。


まとめ

なんとなく使っていた調べていくうちにuseEffectやuseStateなどのReactHookについて詳しく理解することができました。また、Reactの公式ドキュメントはclass構文で書かれていて非常に読みにくいドキュメントでしたが、最近、ベータ版が出てきて非常にまとめられていて読みやすかったです。興味あったり、Reactを実務で使いこなしたい方は是非一度読むといいかもしれません

8
9
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
8
9