環境
React:v18.3.1
Tips
useEffect やイベントハンドラ内のPromiseを検出しない。
Promiseを検出することを確認
簡易サーバを用意
Expressをインストール
HTTPリクエストを受け取ってから2秒後にレスポンスを返す簡易サーバを用意するために、
下記コマンドを実行する。
1 2 3 4 5 6 7 | % mkdir フォルダ名 && cd フォルダ名 % npm init % npm install express // インストールされたExpressのバージョンは4.19.2だった。 % vim app.js |
app.js の内容
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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をコピペしたら、下記コマンドを実行してサーバを起動する。
1 | % node app.js |
サーバ起動後、Webブラウザで http://localhost:3001 へアクセスし、
「ApiServer」と2秒後に表示されれば良い。
Reactのコード
App.tsxの内容(重要でない)
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | 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の内容
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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の内容(重要でない)
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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の内容
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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 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; |
結果(コンソールの表示内容)
01 02 03 04 05 06 07 08 09 10 11 12 13 | 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と表示されることを確認すれば良い。