ReactでuseEffectを使わずにfetchする
useEffect について
React のコードを書くとき、useEffect は極力使わないことが推奨されています。では、fetch でデータ取得の処理を行う場合はどうでしょう?結論として、使う必要はありません。
天気予報を useEffect なしで取得する
useRefで必要な処理やデータを揃えて、useSyncExternalStoreに渡しています。subscribeで受け取っているonStoreChangeがイベント発火のキーになるので、コンポーネントに渡したい値を書き換えたら呼び出してやるという流れです。
useEffect と違って、取得項目を変えて再 fetch する際、任意の処理を用意してクリックイベントなどから直接発火させることができます。これによってuseEffect内で fetch を行うときのような、関連データのバケツリレーが防げます。
ちなみにuseRef + useSyncExternalStoreの組み合わせで書いてますがuseSyncExternalStoreが特殊な hook というわけではないので、useRef + useStateでも同じことは可能です。重要なのはデータ更新タイミングに合わせて、コンポーネントの再レンダリングを発火させることです。
https://github.com/SoraKumo001/next-fetch

1"use client";2import { useRef, useSyncExternalStore } from "react";34export interface WeatherType {5 publishingOffice: string;6 reportDatetime: string;7 targetArea: string;8 headlineText: string;9 text: string;10}1112export default function Page() {13 const storeCtx = useRef({14 subscribe: (onStoreChange: () => void) => {15 storeCtx.onStoreChange = onStoreChange;16 storeCtx.request(storeCtx.defaultParam.id);17 return () => {18 storeCtx.controller.abort();19 };20 },21 request: async (id: number) => {22 const signal = storeCtx.controller.signal;23 storeCtx.result = {24 ...storeCtx.result,25 isLoading: true,26 isError: false,27 data: null,28 };29 storeCtx.onStoreChange();30 await new Promise((result) => setTimeout(result, 1000)); // Simulate network delay31 fetch(32 `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${id}.json`,33 { signal }34 )35 .then((r) => r.json())36 .then((r) => {37 storeCtx.result = { ...storeCtx.result, data: r };38 })39 .catch(() => {40 storeCtx.result = { ...storeCtx.result, isError: true };41 })42 .finally(() => {43 storeCtx.result = { ...storeCtx.result, isLoading: false };44 storeCtx.onStoreChange();45 });46 },47 onStoreChange: () => {},48 controller: new AbortController(),49 result: {50 isLoading: true,51 isError: false,52 data: null as WeatherType | null,53 },54 defaultParam: {55 id: 130000,56 },57 }).current;5859 const result = useSyncExternalStore(60 storeCtx.subscribe,61 () => storeCtx.result,62 () => storeCtx.result63 );6465 const areas = [66 [130000, "東京"],67 [120000, "千葉"],68 [140000, "神奈川"],69 ] as const;7071 return (72 <div className="m-4 w-3xl">73 <div className="flex gap-1">74 {areas.map(([code, name]) => (75 <button76 key={code}77 className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"78 onClick={() => storeCtx.request(code)}79 >80 {name}81 </button>82 ))}83 </div>84 <div className="p-4 border">85 {result.isLoading && <div className="text-blue-600">Loading</div>}86 {result.isError && <div className="text-red-600">Error</div>}87 {result.data && (88 <div className="whitespace-pre-wrap">89 {JSON.stringify(result.data, null, 4)}90 </div>91 )}92 </div>93 </div>94 );95}
まとめ
React でコンポーネント内に単純にデータを保存したいときはuseRefを使います。再レンダリングの発火を伴う State を管理したいときはuseStateを使います。タイミングを明確に操作したい場合や何らかのイベントを伴う場合はuseSyncExternalStoreを使うと良いでしょう。DOM にマウントが完了したあとに実行したい処理にはuseEffectです。それぞれの状況に応じて使い分けることで、より効率的なコードを書くことができます。
ただ一つ言えることは、既存のライブラリを使ったほうが早いということです。