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秒経つとクリーンアップ関数によってignore
がtrue
になる
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での無限ループに注意
-
setCount(count + 1)
が実行されるたびに依存配列のcount
が更新される - 再度
useEffect
が実行される - 以下同様に繰り返される
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, [count]);
冗長なuseEffectをカスタムフックスに切り出す
-
useEffect
部分とUserの型定義を削除して、カスタムフック側に移行 -
const { user, loading } = useFetchUser();
でuser
をloading
を取得する
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;
- 処理は基本同じ
- 最後に
user
とloading
をreturn
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ヶ月前にクローズされました