- useState
- jotai
を利用します。
基本的にはuseStateを利用しpropsで子に受け渡しましょう。
しかしコンポーネントの切り分けや呼び出しがあるとpropsリレー(props drilling) が発生します。
これはその状態に関心がないコンポーネントにも状態がわたる、コンポーネントが密結合になるなどいくつか課題があります。
そういった場合にはjotaiを用いてグローバルなStateを用いてください。
これにより灰色のコンポーネントには状態がわたらず view-component
としてレイアウトに専念することができます。また、末端のコンポーネントも model-component
として状態とその表示に専念できるでしょう。
jotaiはこのような半グローバルな状態共有やHooksと相性の良い状態管理ライブラリです。
類似ライブラリにRecoilがありますが、本テンプレートではよりシンプルなこちらを選定しました。
https://jotai.org/
キャラに独特の可愛さがありますね。
非常にシンプルでuseStateと似た使用感があります。
//store.ts
import { atom } from 'jotai'
const countAtom = atom(0)
//Counter.tsx
import { useAtom } from 'jotai'
const Counter = () => {
const [count, setCount] = useAtom(countAtom);
return (
<div>
{count} <button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
グローバルStateはよく空間の汚染が懸念されます。 その際には状態を共有したい一つ上をProviderで囲ってあげましょう。
import { Provider } from "jotai";
const SomeComponent = () => (
<Provider>
<SomeComponent />
</Provider>
)
Providerはこちらの動画で詳細な使い方が紹介されています。
一方で、これは注意しなくてはならない実装パターンです。せっかくglobalに使用したい状態があったとしても、Providerが存在するとその下では別の状態として扱われてしまいます。
例を挙げると、「スナックバーの表示/非表示の状態が別ものとして扱われてしまう」ケースが挙げられます。
基本は Provider-less mode
で利用し、feature/Component名
・pages/Component名
直下で定義し擬似的なスコープを獲得する。 特殊なケースで Provider
を利用すると良いでしょう。
グローバルな状態とAPI通信は密接な関係にあります。例えば、「ユーザー情報を読み込んでその状態をキャッシュして使い回す」などです。
Reactにはサーバー通信をキャッシュするために他にもメジャーな手段がります。
それが
などです。bulletproof-reactでも紹介されていますのでそちらもご覧ください。
上記のライブラリを端的にまとめると
- 情報をフェッチする
- 情報をライブラリ側にとっておく(キャッシュ化する)
- 次回フェッチ時にキャッシュから情報を返す(無用なAPI通信を避け高速化を図る)
という役割を持つものとなり、状態をグローバルstateにsetする手間が減り便利です。
一方で、キャッシュを使用することになりますので適切に扱わなければ古い情報が返ってくるなどの問題が生じます。
これらのライブラリの根底には stale-while-revalidate
という考え方があります。正しく運用するためにも、導入前に一度触れておくと良いでしょう。参考
上記の中から、本リポジトリではTanstack Query(React Query)
を採用しています。
最も使われているデータフェッチングライブラリであることや、キャッシュのkeyとフェッチングの関数が独立しており細かい調整がしやすいことから選定しました。
使用方法は公式ドキュメントや以下が参考になります。
- 非同期処理に疲れた方に、ReactQueryの処方箋
- React Queryを使いこなすために試したこと
- Filtering a fetched list from an API using React Query
上記のライブラリはリクエストを飛ばす際に
- data
- error
- isLoading(fetching)
などの状態を返却してくれます。また、Reactはあくまで関数ベースです。
したがってコンポーネントの出しわけ、返却値を分岐させるには以下のようなパターンが使えます。
function Example() {
const { isLoading, error, data } = useQuery(['repoData'], () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (isLoading) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
エラーが生じたら失敗の表示を、データがなければローディングを返すという形ですね。
基本はこちらの実装で問題ありませんが、React 18から似た機能として Suspense
が導入されています。また Suspense
はパフォーマンス向上にも寄与します。参考
実装方法としては
- 対応ライブラリを使用する
- Suspenseで囲む
- fallback用のコンポーネントを指定する
という程度ですので負担は大きくありません。
<Suspense fallback={<div>サスペンドしたらこれが表示される</div>}>
{/* ↓サスペンドしなかったらこれが表示される */}
<MyComponent />
</Suspense>
ReactのSuspense対応非同期処理を手書きするハンズオン より引用
しかし登場して間もない技術であること、内部実装が少々特異であるため導入はチームで話し合って決定しましょう。