Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Reactのuse()って何なん?

 Reactのuse()って何なん?

クラスメソッドのReact事情大公開スペシャル#3 で登壇した際の登壇資料です

>https://classmethod.connpass.com/event/316669/

Avatar for morimorikochan

morimorikochan

May 31, 2024
Tweet

More Decks by morimorikochan

Other Decks in Technology

Transcript

  1. ・use()が何かよくわからない人 ・use()がどういう仕組みなのかわからない人 1. use()はPromiseをReact Hooksで扱うためのラッパー 2. use()はPromiseをthrowすることで中の値の取得を実現している 3. use()は何度も実行されるので、use()に渡すPromiseは何回呼び出 されても同じPromiseが渡されるようなキャッシュの仕組みが必要

    4. キャッシュの仕組みを自分で作るの大変そうなので、直接use()を使 う機会はまだしばらくなさそう 対象者 伝えたいこと  🤨Reactのuse()って何なん? 🚨コンテクストの話とSSRの話は今日はしません (できません)
  2. 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>; };
  3. 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()の登場によって、これらより簡単に扱えるようになります。🚀
  4. 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, }); };
  5. 🛁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()) は中の値を返す
  6. 🛁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()) は中の値を返す
  7. 🛁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()) は中の値を返す そうなんです
  8. 🚨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-フックとキャッシュ
  9. 👀これから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
  10. 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/