React基礎編の最後になります。
この記事では、組み込みコンポーネント等まだ解説していない項目について、サンプルを踏まえて解説していきたいと思います。
容量的に前回の下記「コンポーネント編」の記事に盛り込めなかった内容を、この記事で補っていこうと思います。
はじめに
この記事では、下記内容について記載していきます。
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にもキャッチできないエラーがあります。
イベントハンドラーでは、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>
}
では、開発者ツールのコンソールで、ログを確認してみましょう。
出力された各項目の説明です。
項目名 | 単位 | 説明 |
---|---|---|
id | Profilerコンポーネントに設定するid値 | |
phase | 「mount, update, nested-update」コンポーネントの描画理由 | |
actualDuration | ミリ秒 | 子ツリーの描画時間 |
baseDuration | ミリ秒 | メモ化等が行われなかった場合の予想描画時間 |
startTime | タイムスタンプ | 描画開始時刻 |
commitTime | タイムスタンプ | 描画終了時刻 |
おわりに
お疲れ様でした。
長く続いてきた基礎編も、いよいよ今回で終わりになります。
他の基礎編の記事も、忘れた時に振り返る事でReactのアプリの作成は随分捗っていくはずです。
では、次回より、いよいよサンプル画面の作成に移っていきましょう!