Context
Context: teleport ค่าลงไปทั้ง subtree
หัวข้อที่มีชื่อว่า “Context: teleport ค่าลงไปทั้ง subtree”Context ให้ parent ทำให้ค่าหนึ่งพร้อมใช้กับทุก component ที่อยู่ข้างล่าง โดยไม่ต้องส่ง props ผ่านชั้นตรงกลาง คุณสร้าง context, ห่อ subtree ด้วย provider แล้ว descendant ตัวไหนก็อ่านค่านั้นได้ด้วย useContext
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() { const [theme, setTheme] = useState("dark"); return ( // React 19: you can render <ThemeContext> directly as the provider. // (The classic <ThemeContext.Provider> still works too.) <ThemeContext value={theme}> <Page /> </ThemeContext> );}
function Button() { const theme = useContext(ThemeContext); // reads the nearest provider's value return <button className={theme}>Click</button>;}Button ได้ theme ไม่ว่าจะอยู่ลึกแค่ไหน และไม่มี component ตรงกลางต้องส่งต่อค่านั้น นี่คือทางแก้ prop drilling เมื่อ composition เอื้อมไปถึง consumer ไม่ได้
React 19: <Context> คือ provider เอง
หัวข้อที่มีชื่อว่า “React 19: <Context> คือ provider เอง”ก่อน React 19 วิธีเดียวที่จะ provide ค่า context คือ <ThemeContext.Provider value={...}> React 19 ให้ render context object เองเป็น component ได้เลย — <ThemeContext value={...}> — และมันทำหน้าที่เป็น provider เอกสาร createContext บอกตรง ๆ ว่า
Starting in React 19, you can render
<SomeContext>as a provider. In older versions of React, use<SomeContext.Provider>.
.Provider ไม่ได้ถูกลบ — ทั้งสองแบบ provide context เหมือนกัน — แต่โค้ดใหม่ควรเลือกแบบสั้นก่อน
import { createContext, useContext, useState } from 'react';
export const ThemeContext = createContext('light');
export function ThemeApp() { const [theme, setTheme] = useState('dark'); return ( // React 19: <ThemeContext> itself is the provider — no `.Provider`. <ThemeContext value={theme}> <ThemeLabel /> <button onClick={() => setTheme((t) => (t === 'dark' ? 'light' : 'dark'))}> toggle </button> </ThemeContext> );}
function ThemeLabel() { const theme = useContext(ThemeContext); return <span>{theme}</span>;}import { render, screen, cleanup } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';import { ThemeApp } from '../components/ThemeApp';
afterEach(cleanup);
test('React 19 renders <Context> directly as its own provider — no .Provider needed', () => { render(<ThemeApp />); expect(screen.getByText('dark')).toBeInTheDocument();});✓ React 19 renders <Context> directly as its own provider — no .Provider needed
Test Files 1 passed (1) Tests 1 passed (1)use(Context): อ่าน context แบบมีเงื่อนไข
หัวข้อที่มีชื่อว่า “use(Context): อ่าน context แบบมีเงื่อนไข”useContext ต้องถูกเรียกแบบไม่มีเงื่อนไขที่ top level ของ component เหมือน Hook ทั่วไป use อ่าน context แบบเดียวกัน แต่เอกสาร use บอกชัดว่ามันตัดข้อจำกัดนั้นออก
Unlike
useContext,usecan be called within loops and conditional statements likeif.
นั่นสำคัญสำหรับ component ที่ต้องการค่า context แค่ตาม branch เดียว — ด้วย useContext คุณต้องอ่านมันแบบไม่มีเงื่อนไข (แล้วอาจไม่ได้ใช้ หรือต้อง restructure component) แต่ด้วย use คุณอ่านมันตรงจุดที่ต้องการพอดี
import { createContext, use } from 'react';
export const LabelContext = createContext('light');
export function ConditionalThemeReader({ show }: { show: boolean }) { let label = 'hidden'; if (show) { // Unlike useContext, `use` is allowed inside a conditional block. label = use(LabelContext); } return <span>{label}</span>;}import { render, screen, cleanup } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';import { ConditionalThemeReader, LabelContext } from '../components/ConditionalThemeReader';
afterEach(cleanup);
test('use(Context) reads the value when the conditional branch runs', () => { render( <LabelContext value="dark"> <ConditionalThemeReader show={true} /> </LabelContext>, ); expect(screen.getByText('dark')).toBeInTheDocument();});
test('use(Context) is simply never called when the branch does not run', () => { render( <LabelContext value="dark"> <ConditionalThemeReader show={false} /> </LabelContext>, ); expect(screen.getByText('hidden')).toBeInTheDocument();});✓ use(Context) reads the value when the conditional branch runs✓ use(Context) is simply never called when the branch does not run
Test Files 1 passed (1) Tests 2 passed (2)use ยังคงค้นหาขึ้นไปหา provider ที่ใกล้ที่สุด เหนือ component ที่เรียกมัน เหมือน useContext ทุกอย่าง — แค่ไม่ถูกตรึงให้อยู่บนสุดของ function เท่านั้น
เมื่อไร Context เหมาะ — และเมื่อไรกลับเจ็บ
หัวข้อที่มีชื่อว่า “เมื่อไร Context เหมาะ — และเมื่อไรกลับเจ็บ”Context ถูกออกแบบมาสำหรับค่าที่ แชร์กว้างและเปลี่ยนไม่บ่อย: theme, user/auth ปัจจุบัน, locale, config ของ design-system สิ่งที่หลาย component อ่านแต่เปลี่ยนน้อย
จุดที่ต้องระวังคือ performance: เมื่อ value ของ provider เปลี่ยน ทุก consumer จะ re-render — และ React หยุดการ re-render นั้นด้วย memo ไม่ได้ เพราะ consumer subscribe context โดยตรง เอกสาร useContext ระบุเป็น caveat ตรง ๆ
React automatically re-renders all the children that use a particular context starting from the provider that receives a different
value. […] Skipping re-renders withmemodoes not prevent the children receiving fresh context values.
flowchart TB prov["value ของ Provider เปลี่ยน"] --> c1["Consumer A re-render"] prov --> c2["Consumer B re-render"] prov --> c3["Consumer C re-render"] note["memo หยุด context re-render ไม่ได้"] -.-> c1
ดังนั้นการเอา state ที่ เปลี่ยนเร็ว (เช่นค่าที่ update ทุก keystroke หรือทุก animation frame) ไปใส่ context กว้าง ๆ ตัวเดียวคือกับดัก performance — จะ re-render ทั้ง subtree ตลอดเวลา
พิสูจน์: provider value ใหม่ทำให้ memoized consumer re-render ทุกครั้ง
หัวข้อที่มีชื่อว่า “พิสูจน์: provider value ใหม่ทำให้ memoized consumer re-render ทุกครั้ง”กับดักนี้แนบเนียนเพราะไม่ต้องการให้ ข้อมูล เปลี่ยนเลยด้วยซ้ำ — object literal ใหม่ที่มี field เหมือนเดิมทุกอย่างก็พอแล้ว เพราะ React เทียบ value ของ provider ด้วย reference (Object.is) ไม่ใช่ deep equality
import { createContext, useContext, useState, useMemo, memo } from 'react';
type Ctx = { label: string };export const LabelContext = createContext<Ctx>({ label: '' });
export const Consumer = memo(function Consumer({ onRender }: { onRender: () => void }) { const { label } = useContext(LabelContext); onRender(); return <span>{label}</span>;});
export function UnstableValueApp({ onChildRender }: { onChildRender: () => void }) { const [tick, setTick] = useState(0); // ❌ A brand-new object every render — even though `label` never changes. return ( <LabelContext value={{ label: 'fixed' }}> <Consumer onRender={onChildRender} /> <button onClick={() => setTick((t) => t + 1)}>{tick}</button> </LabelContext> );}
export function StableValueApp({ onChildRender }: { onChildRender: () => void }) { const [tick, setTick] = useState(0); // ✅ Memoized — same reference across renders, so memo can bail out. const value = useMemo(() => ({ label: 'fixed' }), []); return ( <LabelContext value={value}> <Consumer onRender={onChildRender} /> <button onClick={() => setTick((t) => t + 1)}>{tick}</button> </LabelContext> );}import { render, screen, cleanup, act } from '@testing-library/react';import { afterEach, expect, test } from 'vitest';import { UnstableValueApp, StableValueApp } from '../components/LabelProvider';
afterEach(cleanup);
test('an inline provider value re-renders a memoized consumer on every unrelated update', () => { let renders = 0; render(<UnstableValueApp onChildRender={() => renders++} />); const baseline = renders;
act(() => { screen.getByRole('button').click(); }); act(() => { screen.getByRole('button').click(); });
// Two unrelated parent re-renders (the tick counter) still re-rendered the // memoized consumer twice, because it got a brand-new context value object // both times — memo cannot see past the reference change. expect(renders - baseline).toBe(2);});
test("a useMemo'd provider value lets memo bail the consumer out", () => { let renders = 0; render(<StableValueApp onChildRender={() => renders++} />); const baseline = renders;
act(() => { screen.getByRole('button').click(); }); act(() => { screen.getByRole('button').click(); });
// Same two clicks, but the context value's reference never changes, so // memo bails the consumer out of both re-renders. expect(renders - baseline).toBe(0);});✓ an inline provider value re-renders a memoized consumer on every unrelated update✓ a useMemo'd provider value lets memo bail the consumer out
Test Files 1 passed (1) Tests 2 passed (2)ลองดูของจริง: provider value identity
หัวข้อที่มีชื่อว่า “ลองดูของจริง: provider value identity”import { createContext, useContext, useState, useMemo, memo } from 'react';
const LabelContext = createContext({ label: 'fixed' });
const Consumer = memo(function Consumer() { const { label } = useContext(LabelContext); return <li>Consumer reads: {label}</li>;});
export default function App() { const [tick, setTick] = useState(0); const [stable, setStable] = useState(false); const stableValue = useMemo(() => ({ label: 'fixed' }), []); const value = stable ? stableValue : { label: 'fixed' }; return ( <LabelContext value={value}> <button onClick={() => setTick((t) => t + 1)}>Unrelated tick: {tick}</button> <button onClick={() => setStable((s) => !s)}> Provider value: {stable ? 'useMemo (stable)' : 'inline object (fresh every render)'} </button> <ul> <Consumer /> </ul> </LabelContext> );}| Component | Renders | Last commit (ms) |
|---|---|---|
| Click Run — commits will appear here. | ||
ตอนโหมดเป็น “inline object” คลิก Unrelated tick สักไม่กี่ครั้ง — แถว Consumer จะเพิ่มทุกครั้งที่คลิก ทั้งที่ label เองไม่เคยเปลี่ยนเลย เพราะ memo มองข้าม object reference ใหม่ของ provider ไม่ได้ (ตามที่พิสูจน์ไว้ข้างบน) สลับโหมดเป็น “useMemo (stable)” แล้วคลิก Unrelated tick อีกครั้ง — แถว Consumer จะหยุดขยับเลย เพราะตอนนี้ provider ส่ง object reference เดิมทุกครั้ง
มีสองวิธีบรรเทาบั๊กกลุ่มนี้:
- Split context. แยกค่าที่ไม่เกี่ยวกันไว้คนละ provider เพื่อให้การเปลี่ยนตัวหนึ่งไม่ re-render consumer ของอีกตัว การ split ที่พบบ่อย: context หนึ่งสำหรับ state และอีกหนึ่งสำหรับ dispatch/updater (updater stable อยู่แล้ว ดังนั้น component ที่ dispatch อย่างเดียวจะไม่ re-render) — บท
useReducerจะสร้าง pattern นี้แบบเต็ม ๆ - ทำให้ value stable. memoize object
valueของ provider (useMemo) เพื่อไม่ให้ได้ reference ใหม่ทุกครั้งที่ parent render ตามที่พิสูจน์ไว้ข้างบน
Context ไม่มี selector
หัวข้อที่มีชื่อว่า “Context ไม่มี selector”การ split context เป็นการแก้แบบ manual ตอน design-time — คุณตัดสินใจไว้ล่วงหน้าว่า field ไหนอยู่ provider ไหน สิ่งที่ Context ทำไม่ได้คือให้ consumer หนึ่งไปแบบไดนามิก subscribe แค่ “field x ของ object นี้” ในขณะที่อีกตัว subscribe แค่ “field y” จาก context เดียวกัน: การเปลี่ยน value ของ provider ตัวไหนก็ตามทำให้ทุก consumer ของ context นั้น re-render หมด ไม่ว่า consumer ตัวนั้นจะอ่าน field ไหนจริง ๆ ก็ตาม
selector ของ external store (useStore(s => s.x)) re-render component แค่เมื่อ ส่วนที่เลือก เปลี่ยน โดยเทียบค่าที่แคบกว่านั้นแทนที่จะเทียบทั้ง store Context ไม่มีกลไกแบบนี้ — มันเทียบทั้ง reference ของ value เมื่อแอปมี consumer หลายตัวที่แต่ละตัวสนใจ slice แคบ ๆ คนละส่วนของ value ก้อนใหญ่ที่เปลี่ยนบ่อย นั่นคือสัญญาณให้ข้าม Context ไปหา external store (บทถัดไป)
Context ไม่ใช่ state manager
หัวข้อที่มีชื่อว่า “Context ไม่ใช่ state manager”ความแตกต่างสำคัญ: Context เป็นกลไก ขนส่ง ไม่ใช่ที่เก็บ state Context ย้ายค่าลงไปตาม tree ไม่ได้ถือหรือ update state ตัว state เองยังอยู่ใน useState หรือ useReducer ใน component ที่เป็น provider “ใช้ Context สำหรับ state management” จริง ๆ แล้วหมายถึง “useState/useReducer ข้างบน แล้วส่งด้วย Context”
function AuthProvider({ children }) { const [user, setUser] = useState(null); // the state lives here return <AuthContext value={{ user, setUser }}>{children}</AuthContext>; // Context just delivers it}Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า context เป็น การกระจายเสียงวิทยุ ไม่ใช่ตู้จดหมาย: provider ส่ง value หนึ่งค่า และทุก component ที่จูนคลื่นเดียวกัน (ทุกการเรียก useContext/use กับ context นั้น ไม่ว่าจะอยู่ที่ไหนข้างล่าง) รับสัญญาณเดียวกันพร้อมกัน ไม่ว่าจะอยากได้ update ครั้งนี้หรือไม่ก็ตาม ไม่มีทางกระจายเสียงแยกความถี่ย่อยภายในสถานีเดียวได้ — นั่นคือความหมายของ “ไม่มี selector” ถ้าผู้ฟังแต่ละคนต้องการความถี่ update ต่างกัน คุณต้องแยกสถานี (split context) หรือใช้เครื่องรับที่ออกแบบมาเพื่อสิ่งนั้นโดยเฉพาะ (selector ของ external store)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ประโยชน์ | ต้นทุน |
|---|---|---|
<Context> provider ตรง ๆ (React 19) | สั้นกว่า ไม่ต้องจำ .Provider | ต้องใช้ React 19+; เวอร์ชันเก่ายังต้องใช้ .Provider |
use(Context) | เรียกแบบมีเงื่อนไข/ใน loop ได้ | คุ้นเคยน้อยกว่า useContext เล็กน้อย; caveat การค้นหาขึ้นไปยังเหมือนเดิม |
| Context กว้างตัวเดียว | ตั้งค่าง่าย | value เปลี่ยนตัวไหนก็ re-render ทุก consumer, memo ช่วยไม่ได้ |
| Split context (state กับ dispatch) | consumer ที่ dispatch อย่างเดียวไม่ re-render เลย | ต้อง wire provider เพิ่ม |
Provider value ผ่าน useMemo | กัน re-render จาก object literal ใหม่ | ต้องดูแล dependency array อีกหนึ่งจุด |
| External store (บทถัดไป) | selector ต่อ slice จริง ๆ | อยู่นอก React; ต้องเรียนรู้ API เพิ่ม |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
memoบน consumer ป้องกันมันจาก context update ได้” ไม่ได้ —memoเช็กแค่ props การเปลี่ยน value ของ context ทำให้ทุก consumer re-render ไม่ว่าจะมีmemoหรือไม่ ตามที่ react.dev ระบุตรง ๆ และพิสูจน์แล้วข้างบน - “ถ้า ข้อมูล ข้างใน value object ไม่เปลี่ยน consumer จะไม่ re-render” จะ re-render ถ้า value เป็น object literal ใหม่ — React เทียบ value ของ provider ด้วย reference ไม่ใช่ deep equality
- “Split context เหมือนกับ selector” Split เป็นการตัดสินใจแบบ manual ตอน compile-time ว่า field ไหนอยู่ไหน ส่วน selector เลือก slice ของ value ก้อนเดียว แบบไดนามิกตอนอ่าน — Context ไม่มีกลไกแบบนั้น
- “Context แทนที่
useState/useReducerได้” มันแค่ขนส่งสิ่งที่useState/useReducerคำนวณไว้แล้ว — state ยังอยู่ใน component ที่เป็น provider
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ review component ที่เป็น Provider ที่ generate มา ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:
- JSX ของ provider สร้าง prop
valueแบบ inline (value={{ ... }}หรือvalue={[a, b]}) แทนที่จะส่ง variable ที่เก็บไว้ในuseMemo/useStateไหม? นั่นคือ reference ใหม่ทุก render และทุก consumer จะ re-render — แม้จะห่อด้วยmemoก็ตาม - context เดียวถือทั้งข้อมูลที่เปลี่ยนช้า (เช่น user ที่ login) และข้อมูลที่เปลี่ยนเร็ว (เช่นตำแหน่ง cursor สด หรือทุก keystroke ของฟอร์ม) ไหม? สิ่งนี้ควรถูก split เป็นสอง context หรือย้ายไป external store
- โค้ดสมมติว่าห่อ consumer ด้วย
memoจะหยุดมันจาก re-render ตอน context เปลี่ยนไหม? มันหยุดไม่ได้ — ตรวจว่า value ของ provider stable หรือเปล่าแทนการเพิ่มmemoที่ consumer use(Context)ถูกเรียกในที่ที่useContextก็ใช้ได้ปกติ (call แบบไม่มีเงื่อนไขที่ top level) ไหม? ทั้งสองแบบถูกต้องเหมือนกันตรงนั้น — แต่ถ้าเห็นuseอยู่ในif/loop นั่นคือเหตุผลที่ต้องใช้useไม่ใช่useContextไม่ใช่ความผิดพลาด- component อ่าน context value ทั้งก้อนแค่เพื่อโชว์ field เดียวไหม ทั้งที่
useReducer+ split context (หรือ selector ของ external store) จะแยกมันออกจาก update ที่ไม่เกี่ยวได้?
🐛 หา bug ในโค้ดนี้
function CartProvider({ children }) {const [items, setItems] = useState([]);const [user, setUser] = useState(null);
return ( <CartContext value={{ items, user, addItem: (item) => setItems((prev) => [...prev, item]), }} > {children} </CartContext>);}เฉลย
object value ถูกสร้างแบบ inline ทุก render ของ CartProvider — object ใหม่เสมอ แม้ว่าทั้ง items และ user จะไม่ได้เปลี่ยนเลยก็ตาม ทุก consumer ของ CartContext จะ re-render ทุกครั้งที่ CartProvider re-render ไม่ว่าจะด้วยเหตุผลอะไร (เช่น parent ที่ไม่เกี่ยวข้อง update state) และ memo บน consumer เหล่านั้นหยุดมันไม่ได้ ตรงตามที่พิสูจน์แล้วข้างบนด้วย UnstableValueApp ทางแก้คือ useMemo(() => ({ items, user, addItem }), [items, user, addItem]) (พร้อมห่อ addItem เองด้วย useCallback) — ตอนนี้ value จะได้ reference ใหม่ก็ต่อเมื่อ items หรือ user เปลี่ยนจริง ๆ เท่านั้น