【React】React基礎編(フック編)

フックは、React16.8から導入された機能になります。
フックを知る事で、関数コンポーネントに様々な機能を追加する事ができます。

今回のは、普段よく使用するフックの使い方を、サンプルを交えながら紹介したいと思います。

はじめに

今回ご紹介するフックは下記になります。
フックの全てではないですが、よく使いそうなフックを記載します。

フック説明
useStateStateと操作する関数を作成する
useReducerStateを複雑な操作で変更する
useContextStateをコンポーネントツリーで共有する
useRefrefを生成し、HTML要素の取得、データの保存を行う
useEffectコンポーネントの描画・破棄のタイミングで処理を行う
useMemo関数の出力をメモ化
useCallback関数をメモ化
memoコンポーネントの出力をメモ化
useTransition更新の優先度を付ける

各種フックの紹介

useState

useStateは、画面の表示に必要な「値の保存」を行います。

const [state, setState] = useState(initial state)

  • state = Stateを格納する変数
  • setState = Stateを変更する関数
  • initial state = Stateの初期値

useStateからは、「Stateの値(text)」と「Stateを更新する関数(setText)」を取得します。
 ※命名は任意です。一般的に、関数には、「set + Stateの名称」となります。

下記は、テキスト入力の値をuseStateで管理しているサンプルです。
useStateを利用する事で、コンポーネントの再描画時に入力した値が反映されます。

import { useState } from 'react';

export default function Sample() {
    const [ text, setText ] = useState('テキスト表示');

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

useReducer

useReducerの特徴は、「Stateの管理を画面の構成から切り離せる」点と、「複雑なStateの管理が行える」点にあります。

const [state, dispatch] = useReducer(reducer, initial state)

  • state = Stateを格納する変数名
  • dispatch = dispatch({type:name}) reducerを呼び出す関数
  • reducer = Stateを更新する関数
  • initial state = Stateの初期値

下記サンプルは、えんぴつの購入本数(number)と単価(price)と合計金額(total)を表示しています。
原価は、えんぴつの購入本数によって変動させています。

この処理をuseStateのみで行うと少し複雑な処理になりますが、「reducer」で一括更新を行う事でシンプルにまとめる事が可能になります。

また、「reducer」を別ファイルに切り出す事により、関数コンポーネントの可読性が上がります。
 ※必ずしも切り出す必要はありません。好みで行って下さい。

import { useReducer } from 'react';
import { reducer, pen } from './SampleReducer'

export default function Sample() {
    const [ state, dispatch ] = useReducer(reducer, pen);

    const AddAction = (e) => {
        e.preventDefault();
        dispatch({
            type:'add',
            payload: {
                number: ++state.number,
                price: state.price
            }
        });
    };
    const SubACtion= (e) => {
        e.preventDefault();
        dispatch({
            type:'sub',
            payload: {
                number: --state.number,
                price: state.price
            }
        });
    };

    // 合計
    const total = state.number * state.price;
    return (
        <div>
            <div>えんぴつ {state.number}本 × 単価{state.price}円 = {total}円</div>
            <input type='button' onClick={AddAction} value='+' />
            <input type='button' onClick={SubACtion} value='ー' />
        </div>
    )
}
// 初期値
export let pen = {
    number: 0,
    price: 100
};

// reducer
export function reducer (state, action) {
    switch(action.type){
        case 'add':
            // 10の倍数の時
            if ( action.payload.number % 10 === 0){
                --action.payload.price;
            }

            return {
                number: action.payload.number,
                price: action.payload.price
            };
        case 'sub':
            // マイナスを回避
            if ( action.payload.number < 0) return pen;

            // 10の倍数の時
            if ( action.payload.number % 10 === 0){
                ++action.payload.price;
            }
            
            return {
                number: action.payload.number,
                price: action.payload.price
            };
        default:
            return state;
    }
};

画面から動作を確認してみましょう。

えんぴつの購入本数にによって、単価が変動しているのが確認できます。

useContext

useContextを使用する事で、コンポーネントツリーのどの階層でもuseContextに登録した値を取得する事が出来ます。

const context = createContext(default)

  • context = コンテキスト
  • default = コンテキストの初期値

const 設定値 = useContext(context)

  • context = createCoutextで生成したcontext

親から孫へ値を渡すとき、通常であればProps経由で子通過し、孫へ値の受け渡しを行います。
子は、使用しないPropsを受け取る事になり、可読性が下がります。

useContextを使用すると、子を経由しなくても孫が直接Context利用して値を取得する事が可能になります。
ゴチャついたPropsが整理される事で、ソースの可読性が上がります

下記は、親の「useState関連」を孫コンポーネント「SampleChild」で取得するサンプルになります。

【親コンポーネントでやる事】

  1. createContext()にて、Contextを作成する。(命名は任意・エクスポートしておく)
  2. 作成したContextより、<Context.Provider>のvalueに格納する値をセットする
import Sample from './Sample'
import { createContext, useState } from 'react'

export const Context = createContext();

function App() {
  console.log('App');
  const [ text, setText ] = useState('初期値');

  return (
    <Context.Provider value={{text, setText}}>
      <Sample/>
    </Context.Provider>
    
  );
}
export default App;

【孫コンポーネントでやる事】

  1. 親ファイルより、Contextをインポートする
  2. インポートしたContextより、useContextを利用して値を受け取る
import { useContext } from 'react';
import { Context } from './App'

export default function Sample() {
    console.log('Sample');
    return (
        <div>
            <SampleChild />
        </div>
    );
}

/*
 * 孫コンポーネント
 */
function SampleChild() {
    console.log('SampleChild');
    const { text, setText } = useContext(Context);

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

画面を確認してみましょう。

無事、親で設定したStateを孫で表示する事が出来ました。

useRef

useRefは、使い方が主に2通りあります。

useReの使い方
  • HTML要素の参照に利用する
  • 一時保存先として利用する

各々をサンプルを使用して確認していきたいと思います。

HTML要素の参照に利用する

useRefよりrefオブジェクトを取得し、HTML要素のref属性に指定します。
これにより、対象の要素を参照が行える為、DOM操作が出来るようになります。

const ref = useRef(null)

  • ref = refオブジェクト

また、この使用方法の場合は、useRefの初期値を「null」とします。

useRefのサンプルになります。
Stateが更新した後、<div>のテキストが「編集後」に変更されます。

import { useState, useRef } from 'react'

export default function Sample() {
    const [ , setCounter ] = useState(0);
    const domRef = useRef(null);

    if ( domRef.current !== null ){
        console.log(domRef.current);
        domRef.current.innerHTML = '編集後';
    }

    return (
        <div>
            <div ref={domRef}>テキスト</div>
            <input type='button' value='更新' onClick={()=> setCounter(pre => pre+1)} />
        </div>
    );
}

画面で確認します。

左は初期表示時の画面サンプルで、右がStateを更新した画面サンプルになります。
初期描画時はrefオブジェクトはnullですが、Stateが更新しコンポーネントが再描画した後は「<div>」要素が参照可能になっています。

HTML要素は「domRef.current」から参照できます。

forwardRef

コンポーネント間を越えて、HTML要素を操作したい時があります。
その時に使用するのが、forwardRefになります。

コンポーネント間でrefオブジェクトの受け渡しが不可な為、「forwardRef」でコンポーネントをラッピングする事で、受け渡しを可能にします。

forwardRef((Props, ref) => { component })

  • Props = コンポーネントに渡す引数
  • ref = refオブジェクト
  • component = コンポーネントの本体

下記はrefオブジェクトを子に渡し、親が操作するサンプルになります。

import { useRef, forwardRef, useEffect } from 'react'

export default function Sample() {
    const iptRef = useRef(null);

    useEffect(() => {
        if ( iptRef.current !== null ) iptRef.current.value = '変更後';
    }, []);
    
    return <SampleText ref={iptRef} text='サンプル' />;
}

const SampleText = forwardRef(({text}, ref) => (
    <label>
        {text}:
        <input type='text' ref={ref} />
    </label>
));

では、画面で確認してみましょう。

テキストボックスに、親がセットした「変更後」が表示されています。

useImperativeHandle

useImperativeHandleを使用する事で、refオブジェクトに定義した関数を、上位コンポーネントで使用できるようになります。

useImperativeHandle(ref, handle [, deps])

  • ref = refオブジェクト
  • handle = 公開する関数
  • deps = 関数に依存する値(配列)

refオブジェクトを引数にするので、「forwardRef」との組み合わせが必須になります。
渡したrefオブジェクトに必要な関数をセットし、上位コンポーネントで実行する事が可能になります。

下記は、子が用意した「テキストを変更関数」を、親が実行するサンプルになります。

import { useRef, forwardRef, useEffect, useImperativeHandle } from 'react'

export default function Sample() {
    const iptRef = useRef(null);

    useEffect(() => {
        iptRef.current?.input();
    }, []);
    
    return <SampleText ref={iptRef} text='サンプル' />;
}

const SampleText = forwardRef(({text}, ref) => {
    const txtRef = useRef(null);

    useImperativeHandle(ref, ()=> {
        const txt = '変更後';
        return {
            input() {
                txtRef.current.value = txt;
            }
        }
    },[]);

    return (
        <label>
            {text}:
            <input type='text' ref={txtRef} />
        </label>
    );
});

結果は、「forwardRef」の時と同じなので、割愛致します。

この「useImperativeHandle」の利点は、子コンポーネントで許可した挙動しか操作する事が出来ない点にあります。
「forwardRef」の利用方法では、親コンポーネントで<input>要素自体が公開されているので、意図しない操作が可能になり、不具合が混入する可能性が考えられます。

コールバックRef

コールバックRefは、useRefを使用しません。

ref属性に直接関数を渡します。
これにより、要素が生成/破棄されたタイミングで関数を呼び出すので、useRefやuseEffenct等を使用しなくても、同様の効果が得られます。

ソースがシンプルになりますね。

下記は、テキストのvalue値と文字色を変更するサンプルになります。

export default function Sample() {
    const ipt = elm => {
        if ( elm !== null ) {
            elm.style.color = 'red';
            elm.value = '変更後';
        }
    }
    
    return (
        <input type='text' ref={ipt} value='' />
    );
}

一時保存先として利用する

useStateと似て非なる使い方です。

useStateは、データが更新されるとコンポーネントが再描画されますが、useRefは再描画行われません。

const ref = useRef(initial state)

  • ref = refオブジェクト
  • initial state = ref.currentの初期値

下記は、useRefを用いてデータを一時保存するサンプルになります。

import { useState, useEffect, useRef } from 'react'

export default function Sample() {
    const [ counter, setCounter ] = useState(0);
    const [ , setDummy ] = useState(0);
    const dtRef = useRef(new Date());

    useEffect( () => {
        dtRef.current = new Date();
    }, [counter]);

    console.log(new Date());
    return (
        <div>
            <div>{dtRef.current.toString()}</div>
            <input type='button' value='日時表示' onClick={()=> setCounter(pre => pre+1)} />
            <input type='button' value='無反応' onClick={()=> setDummy(pre => pre+1)} />
        </div>
    );
}

「日時表示」ボタンをクリックすると、クリックしたタイミングの日時が表示されます。
「無反応」ボタンをクリックしても画面に変化はなく日時は表示されます(更新はされません)。

コンポーネントが再描画されても値が表示される点から、refオブジェクトに日時が保存出来ている事が確認できます。
またconsole.logより、useRefが更新されてもコンポーネントが再描画されない事も確認できます。

useEffect

useEffectは、コンポーネントの「描画時」と「破棄時」に処理を実行します。

ただし、useEffectは公式から微妙な扱いを受けています。

You Might Not Need an Effect – React
The library for web and native user interfaces

公式によると、useEffectは「外部システムと同期させるフックです」と言っていますね。

そんな微妙な扱いの「useEffect関数の構文」です。

useEffect( ()=> { process } [, deps])

  • process = 描画時に実行する処理
  • deps = 依存する値(配列)

サンプルとして、1秒毎にカウントアップする画面を作成しました。

depsに「[](空配列)」を渡す事で、コンポーネント初回描画時のみ実行します。
基本的には、監視対象の変数を配列に加え、その変数が変化した際に実行する様に設定を行います。

useEffect内でコンポーネント破棄時の処理を行う場合、「return」内に記載します。

サンプルのsetIntervalは、コンポーネントが破棄された後も、タイミングによっては「setCounter」を実行します。
その際、コンポーネントが破棄されているのでエラーが発生します。

これを回避するため、コンポーネント破棄時に「clearInterval」を行い、「setInterval」を停止させています。

import { useState, useEffect } from 'react'

export default function Sample() {
    const [ counter, setCounter ] = useState(0);

    useEffect( () => {
        const o = setInterval(
            () => setCounter(pre => pre + 1),
            1000
        );

        return () => {
            clearInterval(o);
        }
    }, []);

    return (
        <div>{counter}</div>
    );
}

今回は、動きを伴う画面構成となっている為、サンプル画面は表示しません。

メモ化

メモ化とは、関数やその結果をキャッシュし、再利用する事を指します。
重い関数の処理をメモ化する事で、パフォーマンスの改善が行えます。

ただし、メモ化を積極的に行う必要はありません。
メモ化自体、ある程度のオーバーヘッドがあるため、必ずしもパフォーマンスの改善に繋がるとは言い難いです。

パフォーマンスの問題が発生してから、改めてメモ化の検討を行うのが良いと思います。

useMemo

useMemoは、関数の結果をメモ化します。

const function = useMemo( ()=> { process } [, deps])

  • process = 描画時に実行する処理
  • deps = 依存する値(配列)

useMemoのサンプルになります。
変数「counter」の更新する度にメモ化する仕組みです。

import { useMemo, useState } from 'react'

export default function Sample() {
    const [ counter, setCounter] = useState(0);
    const [ , setDummy ] = useState(0);
    
    const dom = useMemo(() => {
        console.log('memo化');
        return (
            <div>{counter}</div>
        )
    }, [counter]);

    return (
        <div>
            <div>{dom}</div>
            <input type='button' value='更新' onClick={()=> setCounter(pre => pre+1)} />
            <input type='button' value='無反応' onClick={()=> setDummy(pre => pre+1)} />
        </div>);
}

「無反応」ボタンをクリックしてコンポーネントを再描画しても、ログに「memo化」とは表示されないため、メモ化が正常に動作している事が確認できます。

useCallback

useCallbackは、関数自体をメモ化します。

const function = useCallback( ()=> { process } [, deps])

  • process = 描画時に実行する処理
  • deps = 依存する値(配列)

useCallbackのサンプルになります。
meth1」は、関数をuseCallbackでメモ化しています。
meth2」はメモ化していない為、再描画の度に生成される状態です。

import { useCallback, useState, useRef } from 'react'

export default function Sample() {
    const [ counter, setCounter] = useState(0);
    const [ , setDummy ] = useState(0);
    const chkMeth1 = useRef('');
    const chkMeth2 = useRef('');
    
    const meth1 = useCallback(()=>{
        console.log(counter);
        setCounter(pre => pre+1)
    },[]);

    const meth2 = () => {
        setDummy(pre => pre+1);
    }

    if ( chkMeth1.current !== meth1){
        console.log('meth1');
    }

    if ( chkMeth2.current !== meth2) {
        console.log('meth2');
    }

    chkMeth1.current = meth1;
    chkMeth2.current = meth2;

    return (
        <div>
            <div>{counter}</div>
            <input type='button' value='更新' onClick={meth1} />
            <input type='button' value='無反応' onClick={ meth2 } />
        </div>);
}

「更新」ボタンをクリックすと、ログには「meth2」しか表示されません。
「meth1」はメモ化しているので、「再利用している」事が確認できます。

ただし、「meth1」内部のState「counter」は、「0」のままでカウントアップしていきません。
これは、メモ化した際の影響です。

これを回避するため、第二引数の配列に「counter」を追加して下さい。

const meth1 = useCallback(()=>{
    console.log(counter);
    setCounter(pre => pre+1)
},[ counter ]);

「counter」が更新する度に、「meth1」は再度メモ化を行います。

memo

memoは、コンポーネント自体をメモ化します。

const component = memo(component)

  • component= コンポーネント

memoのサンプルになります。
コンポーネント「FnkDivDom」をメモ化する事で、親コンポーネントが再描画しても「メモ化したコンポーネントは再描画しない」ようにしています。

import { memo, useState } from 'react'

export default function Sample() {
    const [ counter, setCounter] = useState(0);
    const [ , setDummy] = useState(0);

    return (
        <div>
            <input type='button' value='更新' onClick={()=>setCounter(pre => pre + 1)} />
            <input type='button' value='無反応' onClick={()=>setDummy(pre => pre + 1)} />
            <DivDom num={counter} />
        </div>);
}

const DivDom = memo(FnkDivDom);
function FnkDivDom({num}) {
    console.log('孫コンポーネント');
    return <div>孫です {num}</div>
}

「更新」ボタンと「無反応」ボタンをクリックすると、孫コンポーネントが「再描画される/されない」の差が確認できます。

このように、コンポーネントをメモ化する事で、不要な画面更新を抑えることが可能です。
ただし、画面の変更がない場合は、仮想DOMの仕様により「画面描画に関する負荷はない」です。

関数「FnkDivDom」を実行するコスト程度なので、今回のサンプルに関しては、そこまで有用ではないかもしれません。

useTransition

useTransitionは、Stateの更新優先度を制御します。
useTransitionを「未使用/使用」の2つのケースで動作を比較してみます。

const [isPending, startTransition] = useTransition()

  • isPending = 処理の状態
  • startTransition = startTransition(()=>{process}) 優先度が低い処理

useTransitionの未使用ケース

「Dummyコンポーネント」の描画が10秒遅延する様に処理を作成しています。

Reactは、仕様上「Dummyコンポーネント」と「Counterコンポーネント」を同時に更新しようとするので、どちらかの更新が遅れるともう片方のコンポーネントも更新が遅れます。

その為、両者(counter, dummy)のカウント表示が、「10秒遅れて」表示されます。

import { useState } from 'react'

export default function Sample() {
    const [ counter, setCounter] = useState(0);
    const [ dummy, setDummy] = useState(0);

    const upDate = (e) => {
        setCounter(pre => pre + 1);
        setDummy(pre => pre + 1);
    };

    return (
        <div>
            <Counter num={counter} />
            <Dummy num={dummy} />
            <input type='button' value='更新' onClick={upDate} />
        </div>);
}

function Counter({num}) {
    return <div>{num}</div>
}

const stop = time => {
    const start = Date.now();
    while(Date.now() - start < time);
}
function Dummy({num}) {
    stop(10000); // 遅延処理
    return <div>{num}</div>
}

useTransitionを使用したケース

useTransitionを使用したサンプルです。

useTransitionより、「isPending」と「startTransition」を受け取ります。
 ・「isPending」は、遅延の確認用でboolean値です。
 ・「startTransition」は、優先順位の低い処理を設定します。

更新の遅い「Dummyコンポーネント」に「startTransition」を設定する事で、「Counterコンポーネント」は即描画されます。

また、「isPending」を「Dummyコンポーネント」に渡す事で、処理が終わるまでは「now oading!!」を表示する様に修正しています。
これで、ユーザに対し処理中である旨を伝える事ができます。

処理が終われば、Sate「dummy」を表示します。

import { useState, useTransition } from 'react'

export default function Sample() {
    const [ counter, setCounter] = useState(0);
    const [ dummy, setDummy] = useState(0);
    const [ isPending, startTransition ] = useTransition();

    const upDate = (e) => {
        setCounter(pre => pre + 1);
        startTransition(() => setDummy(pre => pre + 1));    
    };

    return (
        <div>
            <Counter num={counter} />
            <Dummy num={dummy} isPending={isPending} />
            <input type='button' value='更新' onClick={upDate} />
        </div>);
}

function Counter({num}) {
    return <div>{num}</div>
}

const stop = time => {
    const start = Date.now();
    while(Date.now() - start < time);
}
function Dummy({num, isPending}) {
    if ( isPending ) return <div>now Loading!!</div>;
    stop(10000);
    return <div>{num}</div>
}

おわりに

思った以上に長くなってしまいました。
他にも紹介していないフックがありますが、使用頻度が低そうなので別の機会に投稿いたします。

需要があれば、各フックの記事を分割等をしてサンプルと説明を増やしたいと思います。

駆け足になりましたが、一旦ここで終わりたいと思います。
お疲れ様でした。