【React】React基礎編(続・コンポーネント編)

React基礎編の最後になります。

この記事では、組み込みコンポーネント等まだ解説していない項目について、サンプルを踏まえて解説していきたいと思います。

容量的に前回の下記「コンポーネント編」の記事に盛り込めなかった内容を、この記事で補っていこうと思います。

はじめに

この記事では、下記内容について記載していきます。

  • Suspense
  • 例外処理
  • Profiler

Suspense

「Suspense」は、コンポーネントの描画が遅延したり等、実際の表示までタイムラグが発生した際、代替UIを表示する機能になります。

遅延している間、代替UIを表示する

下記サンプルは、コンポーネント「SuspenseComp」を5秒遅延させて表示させています。
 ※注)あくまで遅延させるのが目的なので、本来の「lazy」の使い方ではありません。

Suspenseの「fallback」にコンポーネント「Loading」を設定しています。

これにより、「SuspenseComp」を読込んでいる間、、コンポーネント「Loading」が表示させます。
よくある、読み込み中のローディング画面になります。

import { ReactElement, Suspense, lazy } from "react";

// 遅延読み込み用のコンポーネント
const SuspenseComp = lazy(()=> hang(5000).then(()=> import('SuspenseComp')));

export default function SuspenseTest() {
    return (
        <Suspense fallback={<Loading/>}>
            <SuspenseComp />
        </Suspense>
    );
}
// 待機時に表示するためのコンポーネント
function Loading() {
    return <div>ちょっと待って!</div>
}
// 遅延用の処理
function hang(ms: number): Promise<ReactElement> {
    return new Promise(resolve => window.setTimeout(resolve, ms));
}

遅延読み込みするコンポーネント。

import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import TextField from '@mui/material/TextField';

export default function SuspenseComp(){
    return (
        <>
            <Button variant="contained">遅れてきたボタン</Button>
            <Checkbox defaultChecked />
            <TextField id="outlined-basic" label="Outlined" variant="outlined" />
        </>  
    )
}

画面より、「Suspense」の動作を確認したいと思います。

画面を表示すると、コンポーネント「Loading」が表示されます。

5秒経過すると、本来表示する予定の「SuspenseComp」が表示されます。

データ取得の処理が遅く、なかなかコンポーネントの表示に時間がかかる場合、この「Suspense」を使って待機画面を表示することで、ユーザーエクスペリエンスの向上が見込まれます。

例外処理

Reactは、コンポーネントの何れかがエラーとなると、アプリ全体が停止してしまいます。

アプリの停止は、ユーザにとって望ましい状態ではありません。
そこで、発生したエラーを取得し、代替UIを表示してアプリ停止という最悪の状態を阻止しましょう。

react-error-boundary

react-error-boundaryのコンポーネント「ErrorBoundary」を利用する事で、この問題の解決を行いたいと思います。

コマンドプロンプトを起動後、プロジェクトフォルダへ移動します。
その後、下記コマンドを実行します。

npm install react-error-boundary

下記は「ErrorBoundary」の利用サンプルです。
エラー発生時の代替UIを、コンポーネント「ErrorPage」としfallbackにセットしてます。
また、エラー発生時のイベントとして、「onError」時にアラートを表示する様にしています。

エラーの発生方法は、ボタン「エラー発生!」をクリックします。
ボタンをクリックすると、エラー発生用コンポーネント「ErrorCause」でthrowが実行され、ErrorBoundaryがエラーを取得します。

import { useState } from 'react'
import Button from '@mui/material/Button';
import { ErrorBoundary } from 'react-error-boundary'

import ErrorPage from 'errorPage'
import ErrorCause from 'errorCause'

export default function ErrorSample() {
    const [ flg, setFlg ] = useState<boolean>(false);

    const Event = (e: React.MouseEvent<HTMLButtonElement>) => {
        setFlg(pre => !pre);
    }
    return (
        <>
            <div>◆エラーサンプル◆</div>
            <div>
                <Button variant='contained' onClick={Event}>エラー発生!</Button>
            </div>

            <ErrorBoundary fallback={<ErrorPage/>} onError={err => alert(err.message)}>
                <ErrorCause flg={flg} />
            </ErrorBoundary>
        </>
    )
}

代替UIとして下記コンポーネントを表示します。

export default function ErrorPage() {
    return <div>エラー発生の箇所の説明</div>
}

エラーを発生させるコンポーネントです。

type Props = {
    flg: boolean,
};
export default function ErrorCause({flg}: Props) {
    console.log('Error Cause');
    if ( flg ) {
        throw new Error('Error発生の原因はErrorCauseコンポーネント');
    }
    return (
        <div>正常な表示</div>
    );
}

では、画面から動作を確認します。

開発環境だと、エラー画面が表示されてしまい、正確な確認が行えません。
そのため、今回の動作確認はビルドして環境で行います。

※開発環境の場合エラー画面が表示されるので、右上の「×」ボタンをクリックして閉じて下さい。

では、エラーを発生させましょう。
ボタン「エラー発生!」をクリックして下さい。

ボタンクリック後のアラート表示

アラートを閉じた後の画面

エラー発生時、いつものエラー画面が表示されず、onErrorにセットしていたアラートが表示されました(画像左)。

「OK」をクリックすると、コンポーネント「ErrorCause」が代替UIコンポーネント「ErrorPage」に変わっているのが分かります(画像右)。

これで、コンポーネントで発生したエラーによって、アプリ全体が停止する事はなくなりました。
ただ、このErrorBoundaryにもキャッチできないエラーがあります。

【ErrorBoundaryがキャッチできないエラー】
  • イベントハンドラー ※onClick等々(try~catchで対応)
  • 非同期コード ※setTimeout、Promise等々
  • サーバサイドレンダリング
  • ErrorBoundaryの外側で発生したエラー

イベントハンドラーでは、Hookを使用してErrorBoundaryに投げる事が可能ですが、他の項目に関しては、別途対応していく必要があります。

Hookを利用して、イベントハンドラーでのエラーをキャッチする方法

通常の方法では、ErrorBoundaryでエラーを取得する事が出来ませんが、react-error-boundaryからHookの「useErrorBoundary 」をインポートし、さらに関数「showBoundary」を分割代入にて取得する事で、ErrorBoundaryでハンドラーのエラーを取得する事ができます。

下記はサンプルになります。
動作確認に関しては、動きが変わる訳ではないので割愛致します。

import { useErrorBoundary } from 'react-error-boundary'
import Button from '@mui/material/Button';

type Props = {
    flg: boolean,
};
export default function ErrorCause({flg}: Props) {
    const { showBoundary } = useErrorBoundary();

    console.log('Error Cause');
    if ( flg ) {
        throw new Error('Error発生の原因はErrorCauseコンポーネント');
    }

    const click = () => {
        try {
            throw new Error('ハンドラー内でのエラー');
        }catch (e) {
            showBoundary(e);
        }
    }
    return (
        <>
            <Button variant='contained' onClick={click}>ハンドラーでのエラー</Button>
            <div>正常な表示</div>
        </>
    )
}

本来のUIへの復帰

代替UIを表示するのも良いですが、本来のUIを表示させる事も重要です。

下記は、「FallbackComponent」を利用し、本来のUIに復帰させるサンプルになります。
onReset」は、正常にリセットされた後に実行されるコールバック関数で、コンポーネントの状態を更新、クリーンアップさせる処理を組み込みます。

import { useState } from 'react'
import Button from '@mui/material/Button';
import { ErrorBoundary } from 'react-error-boundary'

import ErrorPage from 'errorPage'
import ErrorCause from 'errorCause'

export default function ErrorSample() {
    const [ flg, setFlg ] = useState<boolean>(false);

    // エラー発生イベント
    const Event = (e: React.MouseEvent<HTMLButtonElement>) => {
        setFlg(pre => !pre);
    };

    // onErrorイベント
    const onError = (error:Error, info:React.ErrorInfo) => {
        console.log('onError', error, info.componentStack);
    };

    // onResetイベント
    type onResetType = {
        reason: "imperative-api";
        args: any[];
    } | {
        reason: "keys";
        prev: any[] | undefined;
        next: any[] | undefined;
    };
    const onReset = (details: onResetType) => {
        console.log('onReset ', details);
        setFlg(pre => !pre);
    };

    return (
        <>
            <div>◆エラーサンプル◆</div>
            <div>
                <Button variant='contained' onClick={Event}>エラー発生!</Button>
            </div>

            <ErrorBoundary
                FallbackComponent={ErrorPage}
                onReset= {onReset}
                onError={onError}
                resetKeys={[flg]}>
                <ErrorCause flg={flg} />
            </ErrorBoundary>
        </>
    )
}

代替UIに手を加え、元のUIに復帰させるボタンを配置しました。
クリック時のイベントで、「FallbackComponent」の引数「resetErrorBoundary」を実行します。

import { FC } from 'react'
import { FallbackProps } from 'react-error-boundary'
import Button from '@mui/material/Button';

const ErrorPage: FC<FallbackProps> = ({error,resetErrorBoundary}) => {
    const click = () => {
        console.log('fallback');
        resetErrorBoundary();
    }

    return (
        <div>
            <p>エラーが発生しました!</p>
            <p>{error.message}</p>
            <Button variant='contained' onClick={click}>復帰</Button>
        </div>
    )
}
export default ErrorPage

では、画面から動作を確認します。
今回もビルドした環境で実行します。
 ※最初の画面は特に変化はありません。

では、エラーを発生してみましょう。
ボタン「エラー発生!」をクリックして下さい。

エラーが内部で発生し、代替UIが表示されます。
代替UIには、元のUIに復帰させるボタンが表示されています。

復帰ボタンをクリックして、元のUIを表示させたいと思います。
その際、開発ツールを使ってconsole.logの出力を追ってみます。

復帰後(本来のUIが表示される)

コンソールの出力確認

処理の流れをコンソールの出力で確認します。

「fallback」   :復帰ボタンクリック
 ↓
「onReset」  :onResetコールバック関数実行
 ↓
「Error Cause」:本来のUI表示

onResetで、復帰へのクリーアップ処理を記述すれば、安全に復帰できますね。

Profiler

Profilerは、コンポーネントの描画時間を計測します。
また、Profilerはビルド時に「–profile」オプションを付けない限りは無効となります。

しかし計測する事で、CPUやメモリに若干のオーバーヘッドが発生してしまいます。
計測後は、なるべく撤去した方が良いかもしれません。

下記は、Profilerのサンプルになります。

import { Profiler } from "react";

const ProfilerTest = () => {
    const measure = (
        id:string,
        phase:"mount" | "update" | "nested-update",
        actualDuration:number,
        baseDuration:number,
        startTime:number,
        commitTime:number
    ) => console.log({id,phase,actualDuration, baseDuration, startTime, commitTime});

    return (
        <>
        <Profiler id="profilertest1" onRender={measure}>
            <SampleComp time={5000} />
            <SampleComp time={1000} />
        </Profiler>
        <Profiler id="profilertest2" onRender={measure}>
            <SampleComp time={3000}/>
        </Profiler>
        </>
    )
};
export default ProfilerTest;

// 遅延用コンポーネント
type Props = {time: number}
function SampleComp({time}:Props) {
    const start = Date.now();
    while( Date.now() - start < time);

    return <p>遅延:{time}ms</p>
}

では、開発者ツールのコンソールで、ログを確認してみましょう。

出力された各項目の説明です。

項目名単位説明
idProfilerコンポーネントに設定するid値
phase「mount, update, nested-update」コンポーネントの描画理由
actualDurationミリ秒子ツリーの描画時間
baseDurationミリ秒メモ化等が行われなかった場合の予想描画時間
startTimeタイムスタンプ描画開始時刻
commitTimeタイムスタンプ描画終了時刻

おわりに

お疲れ様でした。
長く続いてきた基礎編も、いよいよ今回で終わりになります。

他の基礎編の記事も、忘れた時に振り返る事でReactのアプリの作成は随分捗っていくはずです。

では、次回より、いよいよサンプル画面の作成に移っていきましょう!