Closed22

React Hooksについて

じゅんじゅん

そもそもReact Hooksとは?

関数コンポーネントとReactの機能を接続する(フック)するためのもの

これまで

  • 状態管理やライフサイクルメソッドを利用するためにはクラスコンポーネントが必要だった
  • Hooksが登場する前は、状態管理やライフサイクルメソッドを関数コンポーネント内で利用できなかった

Hooksの登場

  • 関数コンポーネントとHooksを組み合わて、関数コンポーネント内で状態管理やライフサイクルメソッドが使えるようになった
  • 冗長だったクラスコンポーネントより、関数コンポーネントは簡潔に書ける
  • ロジックの再利用がしやすい
じゅんじゅん

useState

  • 値がメモ化される
    「1」の状態が保存されて、その状態に対して、1プラスされて「2」になる
  • 再レンダリングされる
    セッター関数(setCount)で状態変数(count)を更新した時
import { useState } from "react";

const Counter = () => {
  // カウントを管理するためのstate
  const [count, setCount] = useState<number>(0);

  // ボタンのクリックをハンドルする関数
  const handleCountUp = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button
        onClick={handleCountUp}
      >
        Count Up
      </button>
      <p>カウント:{count}</p>
    </div>
  );
};

export default Counter;
じゅんじゅん

オブジェクトなどのstateの更新

  • useStateにオブジェクトを設定(useState({})
  • inputへの入力のたびにonChangeトリガーのsetForm関数で更新される
Form.tsx
import { ChangeEvent, useState } from "react";

const Form = () => {
  const [form, setForm] = useState({
    firstName: "first",
    lastName: "last",
  });

  return (
    <div>
      <div>
        <label>
          First Name:
          <input
            type="text"
            onChange={(e: ChangeEvent<HTMLInputElement>) =>
              setForm({
                ...form,
                firstName: e.target.value,
              })
            }
          />
        </label>
        <label>
          Last Name:
          <input
            type="text"
            onChange={(e: ChangeEvent<HTMLInputElement>) =>
              setForm({
                ...form,
                lastName: e.target.value,
              })
            }
          />
        </label>
      </div>
      <p>
        {form.firstName}
        <br />
        {form.lastName}
      </p>
    </div>
  );
};

export default Form;

const [form, setForm] = useState({
  firstName: "first",
  lastName: "last",
});

// × ミュータブル(直接破壊的)な操作
form.firstName = "update";

// ○ イミュータブルな操作(スプレット構文)
setForm({
 ...form,
 firstName: e.target.value,
})
じゅんじゅん

useEffect

  • effect -> イベントに対して、外的要因で発火する副作用
    初回レンダリングに対して、データフェッチするなど
import { useState, useEffect } from "react";

const MovePosition = () => {
  // 座標を保存するための状態変数
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    // 座標の状態を更新
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }

    // ポインターの動きに伴ってhandleMove関数が呼ばれる
    window.addEventListener("pointermove", handleMove);
  }, []);

  return (
    <div
      style={{
        position: "absolute",
        backgroundColor: "blue",
        borderRadius: "50%",
        opacity: 0.6,
        pointerEvents: "none",
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -20,
        top: -20,
        width: 50,
        height: 50,
      }}
    ></div>
  );
};

export default MovePosition;
じゅんじゅん

クリーンアップ関数

  • addEventListenerなどのイベントの監視は、アンマウント時(ページ遷移、ページ閉じ、タブ閉じ)に監視を停止(削除)する必要がある
  • パフォーマンス低下につながる
useEffect(() => {
  function handleMove(e) {
    setPosition({ x: e.clientX, y: e.clientY });
  }

  window.addEventListener("pointermove", handleMove);

  // アンマウントされる直前に呼び出されて、イベントの監視を削除する
  return () => {
    window.removeEventListener("pointermove", handleMove);
  };
}, []);
じゅんじゅん

useEffectを使用したデータフェッチ

  • 依存配列(useEffect第2引数)をpersonに設定
  • personの値が変更するたびにuseEffectが発火する
  • fetchBioが呼び出されて、データフェッチされる
DataFetch.tsx
import { useEffect, useState } from "react";
import { fetchBio } from "./fetchBio";

const dataFetch = () => {
  const [person, setPerson] = useState<string>("SampleCode");
  const [bio, setBio] = useState<string | null>(null);

  useEffect(() => {
    const startFetching = async () => {
      const response = await fetchBio(person);
      setBio(response);
    };
    startFetching();
  }, [person]);

  return (
    <div>
      <select onChange={(e) => setPerson(e.target.value)} value={person}>
        <option value="SampleCode">SampleCode</option>
        <option value="TestUser">TestUser</option>
        <option value="SampleUser">SampleUser</option>
      </select>

      <hr />

      <p className="text-black">{bio ?? "Loading..."}</p>
    </div>
  );
};

export default dataFetch;
fetchBio.ts
export async function fetchBio(person: string) {
  // 仮のネットワークレイテンシをシミュレートするために、少し遅延させます。
  await new Promise((resolve) => setTimeout(resolve, 1000));

  const bio = `This is a ${person}'s bio`;
  return bio;
}
じゅんじゅん

データフェッチ時の競合をクリーンアップ関数で解消

クリーンアップ関数の発火タイミング

  • アンマウント時(ページ遷移、タブ閉じ、ページ閉じ)
  • useEffectの依存配列の値が変更される直前

クリック1回

クリックから1秒経つとクリーンアップ関数によってignoretrueになる
setBio(response)が実行されて、表示の値が更新される

クリック2回

クリックから1秒以内に違う値をクリックする
1回目クリック分ではignore = true;にならない
2回目クリック分でignore = true;になる
2回目クリック分として、setBio(response)が実行されて、表示の値が更新される

useEffect(() => {
  // ignore(無視する)を設定
  let ignore = false;

  const startFetching = async () => {
    const response = await fetchBio(person);

    if (ignore!) {
      setBio(response);
    }
  };
  startFetching();

  // 依存配列のpersonが更新される直前に発火
  return () => {
    ignore = true;
  };
}, [person]);
じゅんじゅん

useEffectでの無限ループに注意

  1. setCount(count + 1)が実行されるたびに依存配列のcountが更新される
  2. 再度useEffectが実行される
  3. 以下同様に繰り返される
const [count, setCount] = useState(0);

useEffect(() => {
  setCount(count + 1);
}, [count]);
じゅんじゅん

冗長なuseEffectをカスタムフックスに切り出す

  • useEffect部分とUserの型定義を削除して、カスタムフック側に移行
  • const { user, loading } = useFetchUser();userloadingを取得する
DataFetch.tsx
import { useFetchUser } from "./useFetchuser";

-  interface User {
-   id: number;
-  name: string;
- username: string;
-  email: string;
- address: {
-   city: string;
- };
-}

const dataFetch = () => {
+  const { user, loading } = useFetchUser();
-  const [user, setUser] = useState<User | null>(null);
-  const [loading, setLoading] = useState<boolean>(true);
-
-  useEffect(() => {
-    let isMounted = true; // このフラグはコンポーネントのマウント状態を追跡します
-
-    const fetchUser = async () => {
-      try {
-        const response = await fetch(
-          "https://jsonplaceholder.typicode.com/users/1"
-        );
-        if (!response.ok) {
-          throw new Error("データの取得に失敗しました");
-        }
-        const userData: User = await response.json();
-
-        if (isMounted) {
-          setUser(userData);
-          setLoading(false);
-        }
-      } catch (error) {
-        if (isMounted) {
-          console.error(error);
-          setLoading(false);
-        }
-      }
-    };
-
-    fetchUser();
-
-    // クリーンアップ関数
-    return () => {
-      isMounted = false; // コンポーネントがアンマウントされたらフラグをfalseに設定
-    };
-  }, []); // 空の依存配列
  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>ユーザー情報が見つかりません。</div>;
  }

  return (
    <div>
      <h1>ユーザー情報</h1>
      <p>
        <strong>名前:</strong> {user.name}
      </p>
      <p>
        <strong>ユーザー名:</strong> {user.username}
      </p>
      <p>
        <strong>Email:</strong> {user.email}
      </p>
      <p>
        <strong>都市:</strong> {user.address.city}
      </p>
    </div>
  );
};

export default dataFetch;
  • 処理は基本同じ
  • 最後にuserloadingreturn
hooks/useFetchUser.ts
+ import { useEffect, useState } from "react";
+ 
+ interface User {
+   id: number;
+   name: string;
+   username: string;
+   email: string;
+   address: {
+     city: string;
+   };
+ }
+ 
+ export const useFetchUser = () => {
+   const [user, setUser] = useState<User | null>(null);
+   const [loading, setLoading] = useState<boolean>(true);
+ 
+   useEffect(() => {
+     let isMounted = true; // このフラグはコンポーネントのマウント状態を追跡します
+ 
+     const fetchUser = async () => {
+       try {
+         const response = await fetch(
+           "https://jsonplaceholder.typicode.com/users/1"
+         );
+         if (!response.ok) {
+           throw new Error("データの取得に失敗しました");
+         }
+         const userData: User = await response.json();
+ 
+         if (isMounted) {
+           setUser(userData);
+           setLoading(false);
+         }
+       } catch (error) {
+         if (isMounted) {
+           console.error(error);
+           setLoading(false);
+         }
+       }
+     };
+ 
+     fetchUser();
+ 
+     // クリーンアップ関数
+     return () => {
+       isMounted = false; // コンポーネントがアンマウントされたらフラグをfalseに設定
+     };
+   }, []); // 空の依存配列
+ 
+   return { user, loading };
+ };
じゅんじゅん

useSWR()を使ったキャッシュデータフェッチング

useSQR

  • 速い軽量再利用可能なデータ取得
  • 組み込みのキャッシュとリクエストの重複排除

API

const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options)
  • key : 外部APIのエンドポイント
  • fetch : const fetcher = (url: string) => fetch(url).then((r) => r.json());
DataFetch.tsx
- import { useFetchUser } from "./useFetchuser";

+ const fetcher = (url: string) => fetch(url).then((r) => r.json());

const dataFetch = () => {
-  const { user, loading } = useFetchUser(1);
+  const {
+    data: user,
+    isLoading: loading,
+    error,
+  } = useSWR(`https://jsonplaceholder.typicode.com/users/1`, fetcher);
  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>ユーザー情報が見つかりません。</div>;
  }

  return (
    <div>
      <h1>ユーザー情報</h1>
      <p>
        <strong>名前:</strong> {user.name}
      </p>
      <p>
        <strong>ユーザー名:</strong> {user.username}
      </p>
      <p>
        <strong>Email:</strong> {user.email}
      </p>
      <p>
        <strong>都市:</strong> {user.address.city}
      </p>
    </div>
  );
};

export default dataFetch;
じゅんじゅん

useRef

  • 再レンダリングさせないで値を更新できる
  • 無駄な際レンダリングを防げる
CountUp.tsx
import { useRef } from "react";

const CountUp = () => {
  const ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert(ref.current);
  }

  return (
    <div>
      <input type="text" />
      <button onClick={handleClick}>Click me!</button>
      <p></p>
    </div>
  );
};

export default CountUp;
じゅんじゅん

useRefの使い所

  • DOMの要素に対して何かしらの操作を加えたい時
  • DOMのノード(li > img)に対して、スクロールする(scrollIntoViewの実行)
Scroll.tsx
import { RefObject, useRef } from "react";

const Scroll = () => {
  const listRef: RefObject<HTMLUListElement> = useRef<HTMLUListElement>(null);

  const scrollToIndex = (index: number) => {
    const listNode = listRef.current;
    const imgNode = listNode?.querySelectorAll("li > img")[index];

    imgNode?.scrollIntoView({
      behavior: "smooth",
      block: "nearest",
      inline: "center",
    });
  };

  return (
    <div>
      <nav>
        <button onClick={() => scrollToIndex(0)}>Cat1</button>
        <button onClick={() => scrollToIndex(1)}>Cat2</button>
        <button onClick={() => scrollToIndex(2)}>Cat3</button>
      </nav>
      <div style={{ overflowX: "auto", maxWidth: "700px", margin: "auto" }}>
        <ul
          className="flex items-center justify-between"
          style={{ minWidth: "1300px" }} 
          ref={listRef}
        >
          <ul>
            <li>
              <img
                src="https://api.thecatapi.com/v1/images/search?size=small"
                alt="Cat 1"
                width="200"
                height="200"
              />
            </li>
            <li>
              <img
                src="https://api.thecatapi.com/v1/images/search?size=med"
                alt="Cat 2"
                width="300"
                height="200"
              />
            </li>
            <li>
              <img
                src="https://api.thecatapi.com/v1/images/search?size=small"
                alt="Cat 3"
                width="250"
                height="200"
              />
            </li>
          </ul>
        </ul>
      </div>
    </div>
  );
};

export default Scroll;
じゅんじゅん

input要素をuseStateで管理の場合

  • inputに入力のたびにsetInputTextが実行され、再レンダリングされてしまう
import { useState } from "react";

const InputText = () => {
  const [inputText, setInputText] = useState("");

  const handleClick = () => {
    alert(inputText);
  };

  return (
    <div>
      <input
        type="text"
        className="border-b"
        value={inputText}
        onChange={(e) => setInputText(e.target.value)}
      />
      <button onClick={handleClick}>input入力値を見る</button>
    </div>
  );
};

export default InputText;

input要素をuseRefで管理の場合

  • 入力のたびに再レンダリングされない
import { useRef } from "react";

const InputText = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    alert(inputRef.current?.value);
  };

  return (
    <div>
      <input type="text" className="border-b" ref={inputRef} />
      <button onClick={handleClick}>input入力値を見る</button>
    </div>
  );
};

export default InputText;
じゅんじゅん

forwardRef

  • 通常、関数コンポーネントはrefを直接受け取れないため、forwardRefを使ってrefを子のDOM要素に渡す必要がある
  • 親コンポーネントに対してDOMノードをrefとして設定できる
  • コンポーネントがrefを受け取ってそれを子コンポーネントに転送(forward)できる
親コンポーネント
import { MyVideoPlayer } from "./MyVideoPlayer";
import { useRef } from "react";

const Video = () => {
  // ①useRefでvideoRefを定義
  const videoRef = useRef<HTMLVideoElement>(null);
  return (
    <div>
      <button onClick={() => {}}>Play</button>
      <button onClick={() => {}}>Pause</button>
      <br />
      {/* ② ref={videoRef}として、子コンポーネントにvideoRefを渡す */}
      <MyVideoPlayer
        ref={videoRef}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
        type="video/mp4"
        width="250"
      />
    </div>
  );
};

export default Video;
  • forwardRefを使うことで、親から渡されたrefを子コンポーネント内の特定のDOMノード(この場合は<video>)に渡すことができる
  • (props, ref) => { ... }
    • 最初の引数propsは通常のプロパティ
    • 2番目の引数refは親から渡されたref
子コンポーネント
import { forwardRef } from "react";

type MyVideoPlayerProps = {
  width: string;
  type: string;
  src: string;
};

export const MyVideoPlayer = forwardRef<HTMLVideoElement, MyVideoPlayerProps>(
  (props, ref) => {
    return (
      <video width={props.width} ref={ref}>
        <source src={props.src} type={props.type} />
      </video>
    );
  }
);
じゅんじゅん

useContext

  • propsの穴掘り作業をしなくても、深い階層にデータを受け渡せる
context/AuthContext.tsx
import { createContext, ReactNode, useContext, useState } from "react";
type User = {
  id: string;
  username: string;
  email: string;
};

interface AuthContextType {
  user: User | null;
  login: (userInfo: User) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within a AuthProvider");
  }
  return context;
};

const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<User | null>(null);
  const login = (userInfo: User) => {
    if (
      userInfo.username === "testUser" &&
      userInfo.email === "test@gmail.com"
    ) {
      setUser(userInfo);
    } else {
      console.log("cant logged in");
    }
  };
  const logout = () => {
    setUser(null);
  };
  const contextValue = {
    user,
    login,
    logout,
  };

  return (
    <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
  );
};

export default AuthProvider;
  • 上記で作成したAuthProviderでラップ
  • プロジェクト全体でuser,login,logoutが使用できる
main.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
+  <AuthProvider>
      <App />
+  </AuthProvider>
  </React.StrictMode>
);
  • プロジェクト全体でuser,login,logoutが使用できるので、useAuth()の実行で使用する
UserAuth.tsx
import { useState } from "react";
import { useAuth } from "./context/AuthContext";

const UserAuth = () => {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  // useAuth()の実行でcontextを呼び出して、分割代入で取り出す
  const { user, login, logout } = useAuth();

  // useAuthで所得したlogin関数を実行
  const handleLogin = () => {
    login({ id: "1", username, email });
  };

  return (
    <div>
      {user ? (
        <div>
          <p>ログイン済み:</p>
          <button onClick={logout}>ログアウト</button>
        </div>
      ) : (
        <div>
          <input
            type="text"
            placeholder="Username"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
          <input
            type="email"
            placeholder="Email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <button onClick={handleLogin}>ログイン</button>
        </div>
      )}
    </div>
  );
};

export default UserAuth;
じゅんじゅん

React.memo

React.memoを使わないと

  • Parent Countクリックで子コンポーネントも再レンダリングされる

React.memoを使うと

  • propsで渡されたcount2の値が変化した時だけChildコンポーネントが再レンダリングされる
  • 親コンポーネントであるcount1の値が変化してもChildコンポーネントは再レンダリングされない
Memory.tsx
import { useState } from "react";
import React from "react";

const Memory = () => {
  const [count1, setCount1] = useState<number>(0);
  const [count2, setCount2] = useState<number>(0);

  return (
    <div>
      <button
        onClick={() => setCount1(count1 + 1)}
        className="border-2 px-2 py-2 rounded-md"
      >
        Parent Count
      </button>
      <button
        className="border-2 px-2 py-2 rounded-md ml-2"
        onClick={() => setCount2(count2 + 1)}
      >
        Child Count
      </button>
      <p>Parent: {count1}</p>
      <Child count2={count2} />
    </div>
  );
};

// eslint-disable-next-line react-refresh/only-export-components
const Child = React.memo(({ count2 }: { count2: number }) => {
  //重い処理
  let i = 0;
  while (i < 10000000) i++;
  return <p>Child: {count2}</p>;
});

export default Memory;
じゅんじゅん

useCallback

下記をReact.memoだけでメモ化しようとすると、、、

  • Parent Countクリックで時にuseToggle内のtoggleが再生成される
  • Childコンポーネントが再生成されたtoggleを受け取り、新しい関数扱いになるためmemo化できない
  • Childコンポーネントがレンダリングされてしまう
Memory.tsx
import { useState, memo } from "react";
import { useToggle } from "./hooks/useToggle";

const Memory = () => {
  const [count, setCount] = useState(0);
  const [on, toggle] = useToggle(false);

  console.log("Parent rendered");

  return (
    <div>
      <p>Parent: {count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="border-2 px-2 py-2 rounded-md"
      >
        Parent Count
      </button>
      <Child toggle={toggle} on={on} />
    </div>
  );
};

export default Memory;

// eslint-disable-next-line react-refresh/only-export-components
const Child = memo(({ toggle, on }: { toggle: () => void; on: boolean }) => {
  console.log("Child rendered");
  let i = 0;
  while (i < 10000000) i++;
  return (
    <div>
      <p>Child {on ? "ON" : "OFF"}</p>
      <button onClick={toggle} className="border-2 px-2 py-2 rounded-md">
        Toggle
      </button>
    </div>
  );
});
useToggle.ts
import { useState } from "react";

export const useToggle = (initialState: boolean): [boolean, () => void] => {
  const [state, setState] = useState<boolean>(initialState);

  const toggle = () => {
    setState((state) => !state);
  };

  return [state, toggle];
};

useCallbackでのメモ化

  • 親コンポーネントのレンダリングによるuseToggle内のtoggle再生成が防がれる
  • Childコンポーネントが受け取るtoggleが再生成されたと認識しなくなる
  • React.memoが効いて、Childコンポーネントは再レンダリングされない
useToggle.ts
import { useCallback, useState } from "react";

export const useToggle = (initialState: boolean): [boolean, () => void] => {
  const [state, setState] = useState<boolean>(initialState);

  const toggle = useCallback(() => {
    setState((state) => !state);
  }, []);

  return [state, toggle];
};
じゅんじゅん

useMemo

  • setCount1の実行による際レンダリングによって、double(count2)(重い処理)が実行されてしまう
  • useMemoを使うとcount2が変化した時のみdoubleが再生成される
Count.tsx
import { useState } from "react";

const Count = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const double = (count: number) => {
    // 重い処理
    let i = 0;
    while (i < 100000000) i++;
    return count * 2;
  };

- const doubleCount = double(count2);
+ const doubleCount = useMemo(() => double(count2), [count2]);

  return (
    <div>
      <p>Counter: {count1}</p>
      <button
        onClick={() => setCount1(count1 + 1)}
        className="border-2 px-2 py-2 rounded-md"
      >
        Increment count1
      </button>

      <p>
        Counter: {count2}, {doubleCount}
      </p>
      <button
        onClick={() => setCount2(count2 + 1)}
        className="border-2 px-2 py-2 rounded-md"
      >
        Increment count2
      </button>
    </div>
  );
};

export default Count;
じゅんじゅん

useOptimistic

楽観的UIの更新ができる

import { Message } from "./Lesson6_1";
import React from "react";
import { useRef } from "react";

const Thread = ({
  messages,
  sendMessage,
}: {
  messages: Message[];
  sendMessage: (formData: FormData) => Promise<void>;
}) => {
  const formRef = useRef<HTMLFormElement>(null);

  const formAction = async (formData: FormData) => {
+   addoptimisticMessage(formData.get("message"));
    formRef.current!.reset();
    await sendMessage(formData);
  };

+  const [optimisticMessages, addoptimisticMessage] = useOptimistic(
+    messages,
+    (state: Message[], newMessage: Message) => [
+      ...state,
+      { ...newMessage, sending: true },
+    ]
+  );

  return (
    <div>
+    {optimisticMessages.map((message: Message) => (
-    {messages.map((message: Message, index:number) => (
        <div key={index}>{message.text}</div>
      ))}
      <form action={formAction} ref={formRef}>
        <input
          type="text"
          name="message"
          placeholder="Hello!"
          className="border-2 px-2 py-2 rounded-md"
        />
        <button type="submit" className="ml-2 border-2 px-2 py-2 rounded-md">
          送信
        </button>
      </form>
    </div>
  );
};

export default Thread;
  • addoptimisticMessage(formData.get("message"));で楽観的UIの更新
  • 次行のawait sendMessage(formData);のレスポンスを待たずにUIを更新して表示できる
  • UXが向上する
じゅんじゅん

useTransition

  • UIをブロッキングせずにstateを更新できる
  • 最後にクリックしたものの優先度が高くなる
  • タブやページネーションを高速に切り替える際に使える
selectTab.ts
import { useState, useTransition } from "react";
import TabButton from "./TabButton";
import AboutTab from "./AboutTab";
import PostsTab from "./PostsTab";
import ContactTab from "./ContactTab";

const Tab = () => {
  const [tab, setTab] = useState("about");
+ const [idPending, startTransition] = useTransition();

  function selectTab(nextTab: string) {
+   startTransition(() => {
      setTab(nextTab);
+   });
  }

  return (
    <div>
      <div className="flex gap-4">
        <TabButton
          isActive={tab === "about"}
          onClick={() => selectTab("about")}
        >
          About
        </TabButton>
        <TabButton
          isActive={tab === "posts"}
          onClick={() => selectTab("posts")}
        >
          Posts (slow)
        </TabButton>
        <TabButton
          isActive={tab === "contact"}
          onClick={() => selectTab("contact")}
        >
          Contact
        </TabButton>
      </div>
      <hr className="mt-4" />
      {tab === "about" && <AboutTab />}
      {tab === "posts" && <PostsTab />}
      {tab === "contact" && <ContactTab />}
    </div>
  );
};

export default Tab;
  • startTransitionでラップされたsetTab(nextTab)が他のUIより優先度が低くなる
じゅんじゅん

Suspense

  • 子要素が読み込み完了するまでフォールバックを表示させることができる
  • fallback={}Loading...やコンポーネントなどを設定できる
  • <Suspense>を入れ子構造で設定することで、ロード順に表示できる
Loading.ts
+ import { Suspense } from "react";
import Router from "./Router";

const Loading = () => {
  return (
    <div>
+     <Suspense fallback={<div>Loading...</div>}>
        <Router />
+     </Suspense>
    </div>
  );
};

export default Loading;
ArtistPage.ts
import Albums from "./Albums.js";
import Biography from "./Biography.js";
import Panel from "./Panel.js";
+import { Suspense } from "react";

export default function ArtistPage({ artist }: any) {
  return (
    <>
      <h1>{artist.name}</h1>
      <Biography artistId={artist.id} />
+     <Suspense fallback={<AlbumsGlimmer />}>
        <Panel>
          <Albums artistId={artist.id} />
        </Panel>
+     </Suspense>
    </>
  );
}

function AlbumsGlimmer() {
  return (
    <div className="bg-slate-300">
      <p>Loading...</p>
    </div>
  );
}

じゅんじゅん

useDeferredValue

  • UIの一部の更新を遅延させることができる
search.ts
import { Suspense, useState, useDeferredValue } from "react";
import SearchResult from "./SearchResult";

const Search = () => {
  const [query, setQuery] = useState("");
  const defferedQuery = useDeferredValue(query);

  return (
    <div>
      <label>
        アルバム検索
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          className={`border-2 px-3 py-3 rounded-md`}
        />
        <Suspense fallback={<h2>Loading...</h2>}>
          <SearchResult query={defferedQuery} />
        </Suspense>
      </label>
    </div>
  );
};

export default Search;
  • abと入力する場合、aの入力に基づくデータフェッチの結果が遅延して、相対的にabの値に基づく結果が優先される
  • 入力した文字に対応した結果が表示される
このスクラップは2ヶ月前にクローズされました