環境
React:v18.3.1
Tips
useEffect やイベントハンドラ内のPromiseを検出しない。
Promiseを検出することを確認
簡易サーバを用意
Expressをインストール
HTTPリクエストを受け取ってから2秒後にレスポンスを返す簡易サーバを用意するために、
下記コマンドを実行する。
% mkdir フォルダ名 && cd フォルダ名
% npm init
% npm install express
// インストールされたExpressのバージョンは4.19.2だった。
% vim app.js
app.js の内容
const express = require('express')
const app = express()
const port = 3001
function sendResponse(res) {
res.send('ApiServer');
}
const allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.header(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, access_token'
)
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.send(200)
} else {
next()
}
}
app.use(allowCrossDomain)
app.get('/', (req, res) => {
setTimeout(
sendResponse,
2000,
res
);
// console.log('req = ', req);
// console.log('res = ', res);
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
サーバ起動
上記app.jsをコピペしたら、下記コマンドを実行してサーバを起動する。
% node app.js
サーバ起動後、Webブラウザで http://localhost:3001 へアクセスし、
「ApiServer」と2秒後に表示されれば良い。
Reactのコード
App.tsxの内容(重要でない)
import { ReactNode, useEffect, useLayoutEffect } from 'react';
import Parent from './Parent.tsx';
function App(): ReactNode {
useLayoutEffect(() => {
console.log('App useLayoutEffect');
});
useEffect(() => {
console.log('App useEffect');
});
return (
<>
あいうえお
<Parent />
</>
);
}
export default App;
Parent.tsxの内容
import {
ReactNode, Suspense, useEffect, useLayoutEffect, useState,
} from 'react';
import Child1 from './Child1.tsx';
import Suspend from './Suspend.tsx';
function Parent(): ReactNode {
const [child1Value, setChild1Value] = useState<string>('Child1の初期値');
const [parentValue1, setParentValue1] = useState<string>('Parentの初期値');
function parentBtnClickFunc() {
alert('parentBtnClickFunc');
setChild1Value('親からStateが変更されました');
}
useLayoutEffect(() => {
console.log('Parent useLayoutEffect');
});
useEffect(() => {
console.log('Parent useEffect');
});
return (
<>
<div>
親:
{parentValue1}
</div>
<button type="button" onClick={parentBtnClickFunc}>
親ボタン
</button>
<Child1 childProp={child1Value} parentStateFunc={setParentValue1} />
<Suspense fallback={<p>Now Loading...</p>}>
<Suspend />
</Suspense>
</>
);
}
export default Parent;
Child1.tsxの内容(重要でない)
import { ReactNode, useEffect, useLayoutEffect } from 'react';
type Child1Props = {
childProp: string,
parentStateFunc: Function
};
function Child1({ childProp, parentStateFunc }: Child1Props): ReactNode {
function childBtnClickFunc(): void {
alert('childBtnClickFunc');
parentStateFunc('子からStateが変更されました');
}
useLayoutEffect(() => {
console.log('Child1 useLayoutEffect');
});
useEffect(() => {
console.log('Child1 useEffect start Promise date = ', Date());
});
return (
<>
<div>
子:
{childProp}
</div>
<div>
<button type="button" onClick={childBtnClickFunc}>
子ボタン
</button>
</div>
</>
);
}
export default Child1;
Suspend.tsxの内容
import { ReactNode } from 'react';
let result: string|Response|void;
let blDidThrow: boolean = false;
let promiseData: Promise<string>;
function promiseWrap(promise: Promise<string>) {
console.log('\tpromiseWrap start');
let promiseStatus: string = 'pending';
promise.then(
(resolve) => {
promiseStatus = 'fulfilled';
result = resolve;
console.log('\tpromiseWrap resolve result = ', result);
},
(reason) => {
promiseStatus = 'rejected';
result = reason;
console.log('\tpromiseWrap reason = ', reason);
},
);
if (promiseStatus === 'pending' && !blDidThrow) {
console.log('\tthrow promise');
blDidThrow = !blDidThrow;
throw promise;
}
}
async function fetchData(): Promise<string> {
console.log('\tfetchData start');
const response: Response = await fetch('http://localhost:3001');
const promiseText: Promise<string> = response.text();
return promiseText;
}
function Suspend(): ReactNode {
console.log('\tSuspend promiseWrap date = ', Date());
if (result === undefined) {
promiseWrap(promiseData === undefined ? fetchData() : promiseData);
}
return (
<>
{console.log('\tSuspend return start')}
Suspend.tsx
<br />
showText =
{` ${result}`}
</>
);
}
export default Suspend;
結果(コンソールの表示内容)
Suspend promiseWrap date = Thu May 16 2024 09:19:48 GMT+0900 (日本標準時)
fetchData start
promiseWrap start
throw promise
Child1 useLayoutEffect
Parent useLayoutEffect
App useLayoutEffect
Child1 useEffect start Promise date = Thu May 16 2024 09:19:48 GMT+0900 (日本標準時)
Parent useEffect
App useEffect
promiseWrap resolve result = ApiServer
Suspend promiseWrap date = Thu May 16 2024 09:19:50 GMT+0900 (日本標準時)
Suspend return start
Webブラウザにて表示すると、最初はNow Loadingと表示され、
2〜3秒後にApiServerと表示されることを確認すれば良い。