Context không có shallow compare — bất kỳ consumer nào cũng re-render khi value object đổi reference, kể cả khi field họ đọc không đổi.
Anti-pattern phổ biến:
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('dark')
// value mới mỗi render → mọi consumer re-render
return (
<AuthContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AuthContext.Provider>
)
}Vấn đề: child chỉ cần theme cũng re-render khi user đổi.
Cách fix:
1. Tách context: AuthContext riêng, ThemeContext riêng. Đơn giản và hiệu quả.
2. Memo hóa value: useMemo(() => ({ user, setUser }), [user]).
3. Tách "value" và "setter" thành 2 context: một cho data (đổi nhiều), một cho action (stable). Pattern Kent C. Dodds đề xuất.
4. Dùng store ngoài (Zustand) thay context cho global state — selector có shallow compare built-in.
Hệ quả trên RN nặng hơn web: mỗi re-render → bridge serialize → UIView update. List 100 row mà có context không tách → mỗi keystroke trong TextInput re-render hết list.
Context has no shallow compare — every consumer re-renders when the value object reference changes, even if the field they read did not change.
Common anti-pattern:
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('dark')
// new value object each render → every consumer re-renders
return (
<AuthContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AuthContext.Provider>
)
}Problem: a child that only reads theme re-renders when user changes.
Fixes:
1. Split contexts: dedicated AuthContext and ThemeContext. Simple and effective.
2. Memoize the value: useMemo(() => ({ user, setUser }), [user]).
3. Split "value" and "actions" into two contexts: one for data (changes often), one for actions (stable). Pattern from Kent C. Dodds.
4. Use an external store (Zustand) instead of context for global state — selectors come with built-in shallow compare.
The consequence is worse on RN than the web: each re-render → bridge serialization → UIView update. A 100-row list with an unsplit context re-renders the whole list on every TextInput keystroke.