Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Reactのuse()って何なん?

 Reactのuse()って何なん?

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

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

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/