React Context の値が更新されない
Context の更新が consumer に伝わらない事象は、value の参照が変わっていないか、Provider 自体が毎レンダーで再マウントしているのが典型。
`useState` / `useReducer` を経由して値を渡し、複合オブジェクトは `useMemo` で参照を安定させる。
#react#context#provider#state#rerender
公開:
要約
Context の更新が consumer に届かない事象は、ほぼ「value の参照が変わっていない」か「Provider 自体が再マウントされて値が初期化される」のどちらかに分類できる。
React は Object.is で前回 value と比較し、参照が変わった配下の consumer だけ再レンダーする。let で書き換えた値や ref を value に詰めても発火しない。
よくある原因
- 可変変数 / ref を value に詰める: モジュール変数や
let変数、useRef().currentを value に渡し中身を書き換えるパターンは、参照が同一なので React からは「変わっていない」と見える。 - state を経由していない: 「再レンダーする」のは state(および props)の変化が起点。
setState を経由しない更新は consumer に伝わらない。 - Provider の再マウント: 親で
keyを毎レンダー変える、条件分岐で Provider が外れて戻る、などで内部 state が初期化され、見かけ「値が更新されない」に見える。 - 取り違え: ネストした複数 Provider のうち、
useContextの引数が想定と違う Context を指している。
解決策
1. useState を value に渡す
const UserContext = createContext<{
user: User | null;
setUser: (u: User) => void;
} | null>(null);
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value = useMemo(() => ({ user, setUser }), [user]);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}createContext のリファレンス の通り、useState の戻り値を useMemo で包んでから渡せば、user が変わったときだけ参照も更新される。
2. ref ではなく state で持つ
// NG: ref.current の書き換えは React に通知されない
const ref = useRef({ count: 0 });
return <Ctx.Provider value={ref.current}>...</Ctx.Provider>;
// OK: state を経由
const [count, setCount] = useState(0);
return <Ctx.Provider value={{ count, setCount }}>...</Ctx.Provider>;ref は描画外の可変保存用、「変わったら描画」は state の役割。
3. Provider を安定的にマウント
// NG: key が毎レンダー変わって再マウント
<UserProvider key={Math.random()}>...</UserProvider>
// OK: ルート近くで一度だけマウント
<UserProvider>
<App />
</UserProvider>key の変更は state を意図的にリセットする手段であり、意図せず使うと「更新されない」ではなく「初期値に戻る」現象になる。
4. DevTools で実値を確認
React DevTools → Components → 該当 Provider を選ぶと、現在の value と consume している子孫一覧が見える。
値が更新されているのに consumer が再レンダーしないなら、useContext の引数か上位 Provider の階層を疑う。