shinke1987.net
雑多な備忘録等のはず。
他のカテゴリ・タブ
目次
PR

React:Suspenseコンポーネントの動作確認

2024-05-16 2024-05-16
カテゴリ: React

環境

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 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;

結果(コンソールの表示内容)

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と表示されることを確認すれば良い。

同一カテゴリの記事