This warning occurs when a React Context Provider receives a new value object on every render, causing all consuming components to re-render even when the actual data hasn't changed. It's typically caused by creating objects or functions inline without memoization.
This issue occurs when the `value` prop passed to a React Context Provider changes its reference identity on every render, even though the actual data inside may be the same. React uses strict equality (===) to check if the context value has changed, not deep comparison of the object's contents. When your component re-renders and creates a new object literal or function as the context value, React sees this as a completely different value. This triggers re-renders in all components that consume this context, regardless of whether they actually use the changed data. This is a common performance pitfall that can cause cascading re-renders throughout your component tree, especially in larger applications where many components subscribe to the same context. The issue becomes more severe when the Provider is located high in the component tree and re-renders frequently.
The primary fix is to memoize the entire value object using useMemo to maintain stable object identity:
// ❌ BEFORE: New object on every render
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = (userData) => {
setUser(userData);
};
// This creates a NEW object every render!
return (
<UserContext.Provider value={{ user, login }}>
{children}
</UserContext.Provider>
);
}
// ✅ AFTER: Stable object reference
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = useCallback((userData) => {
setUser(userData);
}, []);
const value = useMemo(
() => ({ user, login }),
[user, login]
);
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}The useMemo hook ensures the value object only gets recreated when its dependencies (user or login) actually change.
Any function included in the context value must be wrapped with useCallback to maintain stable function identity:
function AppProvider({ children }) {
const [state, setState] = useState(initialState);
// ✅ Memoized functions
const updateUser = useCallback((user) => {
setState(prev => ({ ...prev, user }));
}, []);
const logout = useCallback(() => {
setState(prev => ({ ...prev, user: null }));
}, []);
const refreshData = useCallback(async () => {
const data = await fetchData();
setState(prev => ({ ...prev, data }));
}, []);
const value = useMemo(
() => ({
state,
updateUser,
logout,
refreshData
}),
[state, updateUser, logout, refreshData]
);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}Without useCallback, each function would be recreated on every render, changing the value object's identity.
State updater functions from useState and useReducer are already stable and don't need memoization:
function DataProvider({ children }) {
const [data, setData] = useState(null);
// setData is stable - no need for useCallback
const value = useMemo(
() => ({ data, setData }),
[data]
);
return (
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
);
}
// Or with useReducer
function StateProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch is stable - no need for useCallback
const value = useMemo(
() => ({ state, dispatch }),
[state]
);
return (
<StateContext.Provider value={value}>
{children}
</StateContext.Provider>
);
}For complex contexts, consider splitting data and functions into separate providers to minimize re-renders:
// Data context - changes frequently
const UserDataContext = createContext(null);
// API context - functions that rarely change
const UserAPIContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// Functions are memoized once
const api = useMemo(() => ({
login: (userData) => setUser(userData),
logout: () => setUser(null),
updateProfile: (updates) => setUser(prev => ({ ...prev, ...updates }))
}), []);
return (
<UserDataContext.Provider value={user}>
<UserAPIContext.Provider value={api}>
{children}
</UserAPIContext.Provider>
</UserDataContext.Provider>
);
}
// Components only consuming API won't re-render when user data changes
function LogoutButton() {
const { logout } = useContext(UserAPIContext);
return <button onClick={logout}>Logout</button>;
}
// Components consuming data will re-render when user changes
function UserProfile() {
const user = useContext(UserDataContext);
return <div>{user?.name}</div>;
}Use React DevTools to confirm your memoization is working:
1. Open React DevTools and go to the Profiler tab
2. Start recording
3. Trigger actions that cause the Provider's parent to re-render
4. Stop recording and examine the flame graph
Look for:
- Consumer components should NOT re-render when parent re-renders (unless the context value actually changed)
- The Provider itself will still re-render (that's expected)
- Only components using changed portions of data should update
// You can also add console.logs temporarily to debug
function ExpensiveComponent() {
const { user } = useContext(UserContext);
console.log('ExpensiveComponent rendered'); // Should not log unnecessarily
return <div>{user?.name}</div>;
}When NOT to optimize prematurely:
Don't add useMemo/useCallback everywhere by default. Start with simple code and only optimize when you observe actual performance issues. The React team recommends measuring first with the Profiler before optimizing.
Context + React.memo interaction:
Wrapping a component with React.memo will NOT prevent re-renders caused by context changes. If a component uses useContext, it will always re-render when that context value changes, regardless of memo. This is by design - context is an opt-in subscription.
Alternative patterns:
For very complex state management needs, consider:
- Zustand or Jotai for lightweight state management
- Redux Toolkit for application-wide state with more control over re-renders
- React Query/TanStack Query for server state management
- XState for complex state machines
Dependency array gotchas:
Be careful with the useMemo dependency array. If you include objects or arrays that are recreated on every render, your memoization won't work:
// ❌ Still creates new object every render
const value = useMemo(
() => ({ user, settings }),
[user, { theme: 'dark' }] // Object literal in deps!
);
// ✅ Extract stable values
const theme = 'dark';
const value = useMemo(
() => ({ user, settings: { theme } }),
[user, theme]
);TypeScript typing:
When splitting contexts, properly type them to avoid null checks:
const UserContext = createContext<User | null>(null);
function useUser() {
const context = useContext(UserContext);
if (context === null) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite