use() & Data Fetching
use() API ของ React 19
หัวข้อที่มีชื่อว่า “use() API ของ React 19”use() เป็น API ของ React 19 ที่ อ่านค่าของ resource ระหว่าง render — ที่สำคัญที่สุดคือ promise หรือ context เมื่อคุณส่ง promise เข้าไป component จะ suspend ระหว่างที่ promise ยัง pending (แสดง Suspense fallback ที่ใกล้ที่สุด) และคืนค่าที่ resolve แล้วเมื่อ promise settle
import { use, Suspense } from "react";
function Message({ messagePromise }) { const text = use(messagePromise); // suspends until the promise resolves return <p>{text}</p>;}
function Container({ messagePromise }) { return ( <Suspense fallback={<p>Loading…</p>}> <Message messagePromise={messagePromise} /> </Suspense> );}use() แหกกฎข้อหนึ่งที่คุณเรียนกับ hook อื่น: คุณ เรียกแบบมีเงื่อนไขได้ — ใน if หรือใน loop ที่เป็นแบบนั้นได้เพราะไม่ได้ผูกกับ hook slot ตายตัวแบบ useState/useEffect
import { createContext, use } from 'react';import { render, screen } from '@testing-library/react';
const ThemeContext = createContext('light');
function ConditionalReader({ enabled }: { enabled: boolean }) { let theme = 'n/a'; if (enabled) { // A plain `if` around use(Context) — would be a rules-of-hooks violation // for useContext, but use() is explicitly exempt. theme = use(ThemeContext); } return <p data-testid="theme">{theme}</p>;}
test('use(Context) can be called conditionally inside an if', () => { const { rerender } = render( <ThemeContext value="dark"> <ConditionalReader enabled={true} /> </ThemeContext>, ); expect(screen.getByTestId('theme').textContent).toBe('dark');
rerender( <ThemeContext value="dark"> <ConditionalReader enabled={false} /> </ThemeContext>, ); expect(screen.getByTestId('theme').textContent).toBe('n/a');});✓ use(Context) can be called conditionally inside an ifuse() ใน Server Component เทียบกับ Client Component
หัวข้อที่มีชื่อว่า “use() ใน Server Component เทียบกับ Client Component”การอ่าน promise ด้วย use() ใช้ได้ทั้งใน Server และ Client Component — นี่คือกลไกที่ Server Component ใช้ส่ง promise ที่ยังไม่ resolve ลงไปให้ Client Component โดยไม่ต้อง block render ของตัวเอง (ดู React 19: Server Components สำหรับ server/client boundary เอง) การอ่าน context ด้วย use() ต่างออกไป:
“Reading context with
useis not supported in Server Components.” — react.dev,use
เข้าใจได้ทันทีถ้าจำไว้ว่า Server Component ไม่เคย re-render ในเบราว์เซอร์เลย — ไม่มี subscription ที่ live กับ Provider ให้อ่าน ถ้าคุณทำงานกับแอป Next.js โดยเฉพาะ pipeline ของ request/render ที่ตัดสินว่า component ไหนเป็น Server หรือ Client มีสอนลึกใน Next.js Deep Dive: the request lifecycle — คอร์สนี้สอนกลไกระดับ React; routing/rendering pipeline ของ framework เป็นเรื่องของ framework นั้นเอง
cacheSignal (React 19.2, server-only)
หัวข้อที่มีชื่อว่า “cacheSignal (React 19.2, server-only)”cacheSignal() เป็น API ของ React 19.2 สำหรับ React Server Component: คืน AbortSignal ที่ abort เมื่อ React render เสร็จแล้ว (เพื่อให้งานที่อยู่ใน scope ของ cache() — fetch หรือ database call ต่อ render — ถูกยกเลิกได้สะอาด ๆ แทนที่จะทำจนจบแบบไร้ประโยชน์หลังจากไม่มีใครต้องการผลลัพธ์แล้ว)
“
cacheSignalallows you to know when thecache()lifetime is over… It will always returnnullin Client Components… If called outside of rendering, it returnsnull.” — react.dev,cacheSignal
import { cacheSignal } from 'react';
test('cacheSignal() returns null when called outside Server Component rendering (a Client-only environment)', () => { // This probe has no React Server Components runtime — exactly the "Client // Component" case react.dev describes: cacheSignal() always returns null. expect(cacheSignal()).toBeNull();});✓ cacheSignal() returns null when called outside Server Component rendering (a Client-only environment)นั่นคือเรื่องทั้งหมดฝั่ง client: cacheSignal เป็น signal ให้ cache() call ที่ server-render สังเกต ไม่ใช่อะไรที่ Client Component จะเรียกใช้
กฎสำคัญ: cache promise ไว้
หัวข้อที่มีชื่อว่า “กฎสำคัญ: cache promise ไว้”use() ไม่ เริ่ม fetch และไม่ cache อะไรเลย แค่อ่าน promise ที่คุณให้ — ดังนั้น promise นั้น ต้องเสถียรข้าม render การสร้าง promise ใน body ของ component หมายถึง promise ใหม่ทุก render ซึ่งจะ suspend ตลอดกาล (หรือ refetch ไม่หยุด)
// ❌ New promise every render — refetches / never settles.function Bad({ id }) { const data = use(fetch(`/api/${id}`).then(r => r.json())); // ...}
// ✅ The promise is created outside and passed in (cached by the caller/framework).function Good({ dataPromise }) { const data = use(dataPromise); // ...}นี่คือเหตุผลที่ use() ดิบ ๆ มักจับคู่กับ cache เสมอ — ไม่ว่าจะเป็น data layer ของ framework หรือ Map cache ที่ทำเอง — ที่คืน promise instance เดิม สำหรับ request เดิม
Render-as-you-fetch
หัวข้อที่มีชื่อว่า “Render-as-you-fetch”pattern ที่ use() เปิดทางให้คือ render-as-you-fetch: เริ่ม request ก่อนหรือขณะ ที่คุณ render (เช่นใน route loader หรือตอน hover) ส่ง promise ลงไป แล้วปล่อยให้ component อ่าน promise นั้นผ่าน use() ใน Suspense boundary เทียบสามแนวทาง:
flowchart TB
subgraph old["fetch-on-render (useEffect)"]
r1["render"] --> e1["effect รัน"] --> f1["แล้ว fetch ค่อยเริ่ม"] --> r2["re-render พร้อม data"]
end
subgraph new["render-as-you-fetch (use + Suspense)"]
start["เริ่ม fetch ก่อน
(loader / event)"] --> pass["ส่ง promise ลงไป"]
pass --> read["use(promise) suspend
→ Suspense fallback → data"]
end use() ยังอ่าน context ได้ด้วย และต่างจาก useContext ตรงที่เรียกแบบมีเงื่อนไขได้ — สะดวกสำหรับอ่าน context ใน branch
พิสูจน์ความต่างด้าน timing
หัวข้อที่มีชื่อว่า “พิสูจน์ความต่างด้าน timing”claim ของ diagram คือการเริ่ม fetch ก่อนช่วยประหยัดเวลาจริง ไม่ใช่แค่ “รู้สึก” เร็วขึ้น เห็นชัดที่สุดถ้าทำขั้นตอน code-loading ให้ชัดเจน: fetch-on-render เริ่ม fetch ไม่ได้เลยจนกว่า code ของ component เองจะโหลดเสร็จและ mount แล้ว (เพื่อให้ effect รันได้) ในขณะที่ render-as-you-fetch เริ่ม fetch data ขนานกัน กับ code load นั้น ไม่ใช่หลังจากมัน
function delay<T>(ms: number, value: T): Promise<T> { return new Promise((resolve) => setTimeout(() => resolve(value), ms));}
function loadComponentCode() { return delay(20, 'component-code'); // simulates a lazy-loaded chunk}function fetchData(id: string) { return delay(30, `data:${id}`);}
test('fetch-on-render (sequential): code loads, THEN the mount effect starts the fetch', async () => { const start = performance.now(); await loadComponentCode(); // component isn't mounted (so its effect can't run) until code arrives await fetchData('1'); // effect starts the fetch only after mount const elapsed = performance.now() - start; expect(elapsed).toBeGreaterThanOrEqual(45); // ~20 + 30, sequential});
test('render-as-you-fetch: the fetch starts in parallel with the code load, not after it', async () => { const start = performance.now(); const dataPromise = fetchData('1'); // kicked off immediately (e.g. in a route loader) await loadComponentCode(); // runs concurrently with the fetch above await dataPromise; // by the time the component can use() it, it's already well underway const elapsed = performance.now() - start; expect(elapsed).toBeLessThan(40); // ~max(20, 30), not the sum});ผลวัดจริงจากไฟล์นี้ตัวจริง:
✓ fetch-on-render (sequential): code loads, THEN the mount effect starts the fetch ~54ms✓ render-as-you-fetch: the fetch starts in parallel with the code load, not after it ~32msRace condition และ abort
หัวข้อที่มีชื่อว่า “Race condition และ abort”use() ไม่ได้แก้ race ให้คุณ — ถ้าคุณสร้าง promise cache เองและ identity ที่ fetch เปลี่ยนเร็ว (พิมพ์เร็วใน search box, navigate ถี่ ๆ) request เก่าที่ช้ากว่ายัง resolve หลัง request ใหม่ที่เร็วกว่าและ overwrite มันได้ถ้าไม่ยกเลิก AbortController คือทางแก้มาตรฐาน
function fetchSlow(id: string, ms: number, signal: AbortSignal): Promise<string> { return new Promise((resolve, reject) => { const t = setTimeout(() => resolve(`data:${id}`), ms); signal.addEventListener('abort', () => { clearTimeout(t); reject(new DOMException('Aborted', 'AbortError')); }); });}
test('without abort: a slow first request resolves last and overwrites the newer result (the race)', async () => { let latest = ''; const c1 = new AbortController(); const c2 = new AbortController(); const p1 = fetchSlow('first', 40, c1.signal).then((v) => { latest = v; }); const p2 = fetchSlow('second', 10, c2.signal).then((v) => { latest = v; }); await Promise.all([p1, p2]); // 'second' resolves first (fast), then 'first' resolves later and clobbers it. expect(latest).toBe('data:first');});
test('with AbortController: aborting the stale request means it never overwrites the newer result', async () => { let latest = ''; let controller = new AbortController();
async function request(id: string, ms: number) { controller.abort(); // cancel whatever request is in flight controller = new AbortController(); const { signal } = controller; try { const value = await fetchSlow(id, ms, signal); latest = value; } catch (err) { if ((err as DOMException).name !== 'AbortError') throw err; // aborted requests are silently dropped, not applied to state } }
const p1 = request('first', 40); const p2 = request('second', 10); await Promise.allSettled([p1, p2]); expect(latest).toBe('data:second');});✓ without abort: a slow first request resolves last and overwrites the newer result (the race)✓ with AbortController: aborting the stale request means it never overwrites the newer resultปกติคุณจะไม่เขียนเองด้วยมือ
หัวข้อที่มีชื่อว่า “ปกติคุณจะไม่เขียนเองด้วยมือ”นี่คือส่วนที่ต้องซื่อสัตย์ use() ดิบ ๆ + Suspense + promise cache ที่เขียนเองนั้นทรงพลังแต่ระดับต่ำ ในแอปจริงคุณเกือบทุกครั้งจะได้ render-as-you-fetch ผ่าน framework (router/loader ที่ fetch ต่อ route และให้ cache) หรือ data library อย่าง TanStack Query (ที่จัดการ caching, revalidation และ Suspense integration ให้) เรียน use() เพื่อเข้าใจกลไก; ใช้ library เพื่อ ship จริง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ลองนึกถึง use() เป็น ช่องรับจดหมาย ไม่ใช่การโทรศัพท์ fetching แบบ useEffect เหมือนหยิบโทรศัพท์ขึ้นมาโทรก็ต่อเมื่อคุณเดินเข้าห้องไปแล้ว (mount) — โทรก่อนถึงห้องไม่ได้ use() ที่อ่าน promise เหมือนเช็คช่องรับจดหมายที่คนอื่น (loader, parent, framework) เริ่มใส่จดหมายให้ ก่อน คุณมาถึงแล้ว ถ้าจดหมาย (data) ยังไม่มา คุณรอ (suspend) แทนที่จะโทรออกใหม่เอง identity ของ promise คือที่อยู่ของช่องรับจดหมาย — promise ใหม่ทุก render คือช่องรับจดหมายใหม่ที่ว่างเปล่าทุกครั้ง ไม่ใช่ช่องที่กำลังถูกใส่จดหมายจริง ๆ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
use() ดิบ ๆ + cache ที่เขียนเอง | คุมได้เต็มที่ ไม่ต้องพึ่ง dependency เข้าใจกลไกเอง | ต้องดูแล cache, race handling, invalidation เอง |
| router/loader ของ framework | fetch เริ่มก่อน component render ด้วยซ้ำ; มี cache ให้ | ผูกกับ convention การโหลด data ของ framework นั้น |
| data library (TanStack Query ฯลฯ) | caching, revalidation, dedup, Suspense integration พร้อมใช้ | dependency เพิ่มอีกตัวและ mental model ของมันเอง |
use(Context) เทียบกับ useContext | ใช้แบบมีเงื่อนไขได้ ใน if/loop | ไม่รองรับการอ่าน context ใน Server Component |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”use()fetch data ให้คุณ ไม่จริง — มันแค่ อ่าน promise ที่คุณส่งให้ การ fetch, cache และความปลอดภัยตอน retry เป็นความรับผิดชอบของคุณ (หรือ framework) ทั้งหมด- เริ่ม fetch เร็วกว่าแค่ “รู้สึก” เร็วขึ้น ไม่ได้ประหยัดเวลาจริง พิสูจน์แล้วด้านบน: เริ่ม fetch ขนานกับการโหลด code ของ component ย่นเวลาจริงได้วัดได้ เทียบกับรอ mount ก่อนค่อย fetch
use(Context)กับuseContextใช้แทนกันได้ทุกที่ ทั้งคู่อ่าน context เดียวกัน แต่use()ใช้ในเงื่อนไข/loop ได้ ส่วนuseContextใช้ไม่ได้ — และต่างจาก promise ตรงที่use(Context)ไม่รองรับใน Server Component เลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด data-fetching ที่ generate มาซึ่งใช้ use() ให้เช็ค:
- promise ที่ส่งเข้า
use()ถูกสร้างสดใหม่ข้างใน render body ไหม (use(fetch(...))ตรง ๆ) แทนที่จะอ่านจาก cache ที่เสถียร? แบบนั้น suspend ตลอดกาล ตามที่พิสูจน์ด้านบน - เมื่อ identifier ที่ fetch ขึ้นกับเปลี่ยนเร็ว (search query, id ที่เลือก) มีการยกเลิก (
AbortController) สำหรับ request เก่าไหม — หรือ response เก่าที่ช้ากว่า overwrite response ใหม่แบบเงียบ ๆ ได้ไหม? - โค้ดอ่าน context ด้วย
use()ข้างใน Server Component ไหม? react.dev บอกตรง ๆ ว่าไม่รองรับ — ต้องเป็น Client Component cacheSignal()ถูกใช้ (หรือคาดหวังว่าจะคืนอะไรที่มีประโยชน์) ข้างใน Client Component ไหม? มันคืนnullเสมอที่นั่น — เป็น API เฉพาะ server, RSC เท่านั้น
🐛 หา bug ในโค้ดนี้
function SearchResults({ query }) {const [latestQuery, setLatestQuery] = useState(query);const resultsPromise = fetchResults(query);const results = use(resultsPromise);
return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;}เฉลย
fetchResults(query) ถูกเรียกตรงข้างใน render body เลยได้ promise ใหม่ทุก render — บั๊ก use()-ต้องการ-promise-ที่เสถียร ตรง ๆ นอกจากนั้น แม้ promise จะถูก cache ถูกต้อง ก็ยังไม่มีการยกเลิก: ถ้า user พิมพ์เร็ว request เก่าของ query ก่อนหน้าอาจ resolve หลัง request ใหม่และแสดงผลลัพธ์ที่ค้างแบบเงียบ ๆ ทางแก้มีสองส่วน: cache promise ต่อ query (เช่นใน Map ที่ key ด้วย query string เป๊ะ) เพื่อให้ use() ได้ reference ที่เสถียร และยกเลิก request เก่าที่ค้างอยู่ด้วย AbortController ทุกครั้งที่ query เปลี่ยน ตรงตามที่พิสูจน์ใน race/abort test ด้านบน