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

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

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

環境

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

同一カテゴリの記事