Open6
react18環境下でのSuspense
とりあえず環境構築
今回はフロント挙動だけ確認したいのでViteで雑に作る
yarn create vite . --template react-swc-ts
package.json
{
"name": "react18-sandbox",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.4"
}
}
前提として読んだ記事
react17では例えばこんな感じに書いていた(Fetch-on-render)
App.tsx
import { useState, useEffect } from 'react';
import './App.css'
const sleep = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
export default function App() {
const [userName, setUserName] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
sleep(2000).then(() => { // データ取得待ちを擬似的に再現
setUserName('Taro') // データ取得が完了したらユーザー名をセット
}).finally(() => {
setIsLoading(false);
});
}, []);
return (
<div>
{
isLoading ? (
<h1>Loading...</h1>
) : (
<h1>{userName}</h1>
)
}
</div>
);
}
react18でSuspenseを利用してベタっと書くとこんな感じ
App.tsx
import { Suspense } from 'react';
import './App.css'
const sleep = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
const fetchUserName = () => {
let status = 'pending';
let result = '';
const suspender = sleep(2000).then(() => {
status = 'resolved';
result = 'Taro';
}).catch(e => {
status = 'rejected';
result = e;
});
return () => {
if (status === 'pending') throw suspender;
if (status === 'rejected') throw result;
return result;
};
};
const userName = fetchUserName(); // 非同期処理の状態を保持
// コンポーネントとして切り出される
const UserNameElement = () => {
const name = userName();
return <h1>{name}</h1>;
};
// App内の記述は簡潔になる
export default function App() {
return (
<div>
<Suspense fallback={<h1>Loading...</h1>}>
<UserNameElement />
</Suspense>
</div>
);
}
- (データ取得が終わっていて)UserNameElementがレンダリングできる場合はUserNameElementが表示される
- (データ取得が終わっていなくて)UserNameElementがレンダリングできない場合はUserNameElementのレンダリングがサスペンド(中断)されてfallbackに指定した要素が表示される
- つまりuserName()の戻りがPromiseである場合
- この場合、reactが後から再レンダリングしようとする
- 再レンダリング時にUserNameElementは毎回最初からレンダリングされるので、非同期処理の状態をコンポーネント外に持つ必要がある
-
const userName = fetchUserName();
のところ
-
- 再レンダリング時にUserNameElementは毎回最初からレンダリングされるので、非同期処理の状態をコンポーネント外に持つ必要がある
- fetchUserNameに相当する部分はある程度汎化して切り出したりSuspenseネイティブなライブラリを利用することになるイメージ
- UserNameElementは単独のコンポーネントとして定義された(ので別ファイルに切り出せば良い)
- App内では(データのローディングに関わる)state管理およびuseEffectの利用をなくすことができ、コンポーネントの取り回しだけを行う記述になった