ข้ามไปยังเนื้อหา

Context

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 วิธีเดียวที่จะ 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 เหมือนกัน — แต่โค้ดใหม่ควรเลือกแบบสั้นก่อน

src/components/ThemeApp.tsx
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>;
}
src/__tests__/context-direct-provider.test.tsx
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)

useContext ต้องถูกเรียกแบบไม่มีเงื่อนไขที่ top level ของ component เหมือน Hook ทั่วไป use อ่าน context แบบเดียวกัน แต่เอกสาร use บอกชัดว่ามันตัดข้อจำกัดนั้นออก

Unlike useContext, use can be called within loops and conditional statements like if.

นั่นสำคัญสำหรับ component ที่ต้องการค่า context แค่ตาม branch เดียว — ด้วย useContext คุณต้องอ่านมันแบบไม่มีเงื่อนไข (แล้วอาจไม่ได้ใช้ หรือต้อง restructure component) แต่ด้วย use คุณอ่านมันตรงจุดที่ต้องการพอดี

src/components/ConditionalThemeReader.tsx
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>;
}
src/__tests__/use-context-conditional.test.tsx
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 ถูกออกแบบมาสำหรับค่าที่ แชร์กว้างและเปลี่ยนไม่บ่อย: 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 with memo does 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
value ของ context เปลี่ยนทีทำให้ทุก consumer re-render

ดังนั้นการเอา state ที่ เปลี่ยนเร็ว (เช่นค่าที่ update ทุก keystroke หรือทุก animation frame) ไปใส่ context กว้าง ๆ ตัวเดียวคือกับดัก performance — จะ re-render ทั้ง subtree ตลอดเวลา

กับดักนี้แนบเนียนเพราะไม่ต้องการให้ ข้อมูล เปลี่ยนเลยด้วยซ้ำ — object literal ใหม่ที่มี field เหมือนเดิมทุกอย่างก็พอแล้ว เพราะ React เทียบ value ของ provider ด้วย reference (Object.is) ไม่ใช่ deep equality

src/components/LabelProvider.tsx
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>
);
}
src/__tests__/context-value-identity.test.tsx
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)
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>
);
}
Babel 7.29.9 (jsdelivr) · React 19.3.0 dev build (esm.sh, pinned)
ComponentRendersLast 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 ตามที่พิสูจน์ไว้ข้างบน

การ 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 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
}

มองว่า 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

เมื่อ 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 เปลี่ยนจริง ๆ เท่านั้น

Context แก้ปัญหาอะไร?
เกิดอะไรกับ consumer เมื่อ value ของ context provider เปลี่ยน?
ทำไม Context ถึง "ไม่ใช่ state manager"?
อะไรคือวิธีบรรเทา performance ของ context re-render ที่ดี?
use(Context) ทำอะไรได้ที่ useContext(Context) ทำไม่ได้?
คลิกปุ่มสองครั้ง — Consumer ที่ห่อด้วย memo re-render กี่ครั้ง?
const value = useMemo(() => ({ label: 'fixed' }), []);
return (
  <LabelContext value={value}>
    <Consumer onRender={onChildRender} />
    <button onClick={() => setTick((t) => t + 1)}>{tick}</button>
  </LabelContext>
);
คลิกแบบเดิม แต่ value เป็น object literal inline ที่ถูกสร้างทุก render Consumer ที่ memoize ไว้ re-render กี่ครั้งจากสองคลิก?
return (
  <LabelContext value={{ label: 'fixed' }}>
    <Consumer onRender={onChildRender} />
    <button onClick={() => setTick((t) => t + 1)}>{tick}</button>
  </LabelContext>
);