Share
Embed
Transcript
19 slides
-
・use()が何かよくわからない人 ・use()がどういう仕組みなのかわからない人 1. use()はPromiseをReact Hooksで扱うためのラッパー 2. use()はPromiseをthrowすることで中の値の取得を実現している 3. use()は何度も実行されるので、use()に渡すPromiseは何回呼び出 されても同じPromiseが渡されるようなキャッシュの仕組みが必要
4. キャッシュの仕組みを自分で作るの大変そうなので、直接use()を使 う機会はまだしばらくなさそう 対象者 伝えたいこと 🤨Reactのuse()って何なん? 🚨コンテクストの話とSSRの話は今日はしません (できません) -
useEffect+useState a ライブラリでよしなに 🤨今までPromiseはどう扱ってたん? const AppContent = () => {
const [myName, setMyName] = useState<null | string>(null); useEffect(() => { fetchMyName().then((_myName) => setMyName(_myName)); }, []); if (myName === null) return <p>loading...</p>; return <p>{myName}</p>; }; const AppContent = () => { // TanStack Query const { data: myName, loading } = useQuery({ queryKey: ["myName"], queryFn: () => fetchMyName(), }); if (loading) return <p>loading...</p>; return <p>{myName}</p>; }; -
useEffect+useState a ライブラリでよしなに 🤨今までPromiseはどう扱ってたん? const AppContent = () => {
const [myName, setMyName] = useState<null | string>(null); useEffect(() => { fetchMyName().then((_myName) => setMyName(_myName)); }, []); if (myName === null) return <p>loading...</p>; return <p>{myName}</p>; }; const AppContent = () => { // TanStack Query const { data: myName, loading } = useQuery({ queryKey: ["myName"], queryFn: () => fetchMyName(), }); if (loading) return <p>loading...</p>; return <p>{myName}</p>; }; const AppContent = () => { const myName = use(fetchMyName()); return <p>{myName}</p>; }; 🚀use()の登場によって、これらより簡単に扱えるようになります。🚀 -
const AppContent = () => { const myName = use(fetchMyName());
return <p>{myName}</p>; }; 🤨async/awaitは書かなくてええの? Q: 非同期な処理の結果を待つためには async/awaitの記述が必要なはずでは? A: 書かなくていいです // こんな感じの const registerUser = async (email: string) => { const data = await fetchUserData(email); }; const fetchUserData = async (email: string) => { return await axios.$get("/users", { email, }); }; -
Q: なんでasync/awaitを書かなくてもPromise の中の値が取得できるの? A: Reactはこの問題を • 値が解決(fullfiled)していない場合は Promiseをthrowさせる(=大域脱出する) • 値が解決した場合は中の値を返却する
ことで解決しています const AppContent = () => { const myName = use(fetchMyName()); return <p>{myName}</p>; }; 🤨async/awaitは書かなくてええの? -
🛁use()で非同期処理を呼び出した際のフロー const AppContent = () => { // 1 const
myName = use(fetchMyName()); // 2, 3, 5 return <p>{myName}</p>; }; export const App = () => { return ( <ErrorBoundary fallback={<p>エラー</p>}> <Suspense fallback={<div>loading...</div>}> // 4 <AppContent /> </Suspense> </ErrorBoundary> ); }; 1. <AppContent /> がレンダリングされる 2. use(fetchMyName()) が実行される 3. use() の中でthrow promise; される 4. 親コンポーネントに伝播し、<Suspense> で捕ま り、フォールバック(loading... )が表示される 5. 1のPromiseが解決(fullfiled)されると、再 び<AppContent /> がレンダリングされ、その時 use(fetchMyName()) は中の値を返す -
🛁use()で非同期処理を呼び出した際のフロー const AppContent = () => { // 1 const
myName = use(fetchMyName()); // 2, 3, 5 return <p>{myName}</p>; }; export const App = () => { return ( <ErrorBoundary fallback={<p>エラー</p>}> <Suspense fallback={<div>loading...</div>}> // 4 <AppContent /> </Suspense> </ErrorBoundary> ); }; あれ?fetchMyName() が2回呼び出されてない? 1. <AppContent /> がレンダリングされる 2. use(fetchMyName()) が実行される 3. use() の中でthrow promise; される 4. 親コンポーネントに伝播し、<Suspense> で捕ま り、フォールバック(loading... )が表示される 5. 1のPromiseが解決(fullfiled)されると、再 び<AppContent /> がレンダリングされ、その時 use(fetchMyName()) は中の値を返す -
🛁use()で非同期処理を呼び出した際のフロー const AppContent = () => { // 1 const
myName = use(fetchMyName()); // 2, 3, 5 return <p>{myName}</p>; }; export const App = () => { return ( <ErrorBoundary fallback={<p>エラー</p>}> <Suspense fallback={<div>loading...</div>}> // 4 <AppContent /> </Suspense> </ErrorBoundary> ); }; あれ?fetchMyName() が2回呼び出されてない? 1. <AppContent /> がレンダリングされる 2. use(fetchMyName()) が実行される 3. use() の中でthrow promise; される 4. 親コンポーネントに伝播し、<Suspense> で捕ま り、フォールバック(loading... )が表示される 5. 1のPromiseが解決(fullfiled)されると、再 び<AppContent /> がレンダリングされ、その時 use(fetchMyName()) は中の値を返す そうなんです -
🚨use()に渡すPromiseはキャッシュされている必要がある let cachedPromise: Promise<string> | null = null; const fetchMyName:
() => Promise<string> = () => { if (cachedPromise === null) { cachedPromise = axios.get("/my-name") return cachedPromise; } return cachedPromise; }; レンダリングの度に新しい Promiseが作成される =意図せずAPIが呼ばれちゃう😇 なのでPromiseを作成する側でキャッシュが必須 雑にキャッシュを実装するとこんな感じ →→→→→→ もしレンダリングの度に新しい Promiseを作ってると、こん な感じで怒られます >Warning: A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework. https://azukiazusa.dev/blog/promise-context-value-react-hook/#use-フックとキャッシュ -
👀これからuse()はデファクトスタンダードになる? import { useSuspenseQuery } from '@tanstack/react-query' const { data
} = useSuspenseQuery({ queryKey, queryFn }) (個人の感想です) これから利用されていくと思いますが 開発者が直接use()を利用することはしばらくの 間はなく 間接的にTanStack Query v5(右図)などのライ ブラリを通じて利用することになるのではないか なーと思ってます。 これはPromiseのキャッシュ実装がめちゃくちゃ 大変だからです https://tanstack.com/query/latest/docs/framework/ react/reference/useSuspenseQuery -
const useCustom = <T,>(promise: Promise<T>): T => { const [,
setKey] = useState(0); const onForceUpdate = () => { setKey((key) => key + 1); }; const isCached = cache !== null; if (isCached) { if (resolvedValue === null) throw cache; if (resolvedValue.status === "fullfiled") { return resolvedValue.value as T; } if (resolvedValue !== null && resolvedValue.status === "rejected") { throw resolvedValue.error; } console.warn("不明なステータス", resolvedValue); } 🛠use()を自作してみたよ promise .then((v) => { resolvedValue = { status: "fullfiled", value: v }; onForceUpdate(); }) .catch((error) => { resolvedValue = { status: "rejected", error }; onForceUpdate(); }); cache = promise; throw promise; }; https://dev.classmethod.jp/articles/ create-custom-use-in-react/ -
📽まとめ • (誤解を恐れずにいうと)use()はPromiseをReact Hooksで扱うためのラッパー • use()はPromiseをthrowすることで中の値の取得を実現している • use()は何度も実行されるので、use()に渡すPromiseは何回呼び出されても同じ Promiseが渡されるようなキャッシュの仕組みが必要 •
キャッシュの仕組みを自分で作るの大変そうなので、直接use()を使う機会はまだしば らくなさそう