Back to Blog

React useEffect & useCallback in 2026: Stop Unnecessary Re-renders (React 19 Guide)

AI Transformation Lead
  • React
  • React 19
  • useEffect
  • useCallback
  • useMemo
  • Performance
  • Web Development
  • TypeScript
React hooks performance optimization: useCallback, useMemo, and React.memo patterns

Unnecessary effect re-runs are one of the most common React performance issues. They manifest as duplicate API calls, flickering UIs, and infinite render loops. Understanding why they happen, and when to reach for each memoization tool, is essential for production-grade React.

The Root Cause

React compares effect dependencies using Object.is (strict reference equality). Functions are recreated on every render by default, so their reference changes every time, even if the function body is identical.

tsx
function MyComponent({ userId }: { userId: string }) { const [data, setData] = useState(null); // ❌ Problem: fetchUser is a new function instance on every render const fetchUser = () => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setData); }; // The effect re-runs on EVERY render because fetchUser changes every render useEffect(() => { fetchUser(); }, [fetchUser]); return <div>{data?.name}</div>; }

Fix 1: useCallback: Stabilize Function References

useCallback returns a memoized version of the function that only changes when its declared dependencies change:

tsx
import { useEffect, useCallback, useState } from 'react'; function MyComponent({ userId }: { userId: string }) { const [data, setData] = useState(null); // ✅ Only recreated when userId changes const fetchUser = useCallback(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setData); }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]); // Stable unless userId changes return <div>{data?.name}</div>; }

Fix 2: Move the Function Inside the Effect

If the function is only used inside one effect, move it inside. No dependency array entry needed:

tsx
useEffect(() => { // Function lives here, no memoization required async function fetchUser() { const res = await fetch(`/api/users/${userId}`); const json = await res.json(); setData(json); } fetchUser(); }, [userId]); // userId is the only real dependency

This is often the cleanest solution and preferred by the React team for effects with a single-use function.

Fix 3: useMemo: Memoize Expensive Derived Values

useCallback is syntactic sugar for useMemo returning a function. Use useMemo directly for computed values:

tsx
function ProductList({ products, searchTerm }: Props) { // ❌ Recomputes on every render const filtered = products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) ); // ✅ Only recomputes when products or searchTerm changes const filtered = useMemo(() => products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) ), [products, searchTerm] ); return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>; }

Fix 4: React.memo: Skip Child Re-renders

Wrap child components that receive stable props to prevent re-renders when the parent re-renders:

tsx
const UserCard = React.memo(({ user, onSelect }: UserCardProps) => { console.log('UserCard render'); // Should only log when user or onSelect changes return ( <div onClick={() => onSelect(user.id)}> {user.name} </div> ); }); function ParentComponent({ users }: { users: User[] }) { const [selected, setSelected] = useState<string | null>(null); // ✅ Stable: memoize the callback passed to memo'd child const handleSelect = useCallback((id: string) => { setSelected(id); }, []); // No deps, setSelected is always stable return ( <ul> {users.map(user => ( <UserCard key={user.id} user={user} onSelect={handleSelect} /> ))} </ul> ); }

React.memo + useCallback is the canonical pattern for preventing child re-renders.

Fix 5: Custom Hooks: Return Stable References

When building custom hooks, always memoize the values you return:

tsx
function useUserData(userId: string) { const [data, setData] = useState<User | null>(null); const [loading, setLoading] = useState(false); const refetch = useCallback(async () => { setLoading(true); try { const res = await fetch(`/api/users/${userId}`); setData(await res.json()); } finally { setLoading(false); } }, [userId]); useEffect(() => { refetch(); }, [refetch]); return { data, loading, refetch }; // refetch is stable per userId }

React 19: The Compiler Handles Most of This Automatically

React 19 ships the React Compiler, a Babel/SWC plugin that analyzes your components and automatically inserts memoization where needed. It eliminates most manual useCallback and useMemo calls.

Enable it in next.config.ts (Next.js 15+):

typescript
// next.config.ts const nextConfig = { experimental: { reactCompiler: true, }, }; export default nextConfig;

With the compiler enabled, the following code is automatically optimized without any useCallback:

tsx
// React Compiler makes this safe, no manual memoization needed function AutoOptimized({ userId }: { userId: string }) { const [data, setData] = useState(null); const fetchUser = () => { fetch(`/api/users/${userId}`).then(r => r.json()).then(setData); }; useEffect(() => { fetchUser(); }, [userId]); return <div>{data?.name}</div>; }

The compiler is opt-in for now. Until you enable it, the manual patterns above remain necessary.

Profiling: Find What’s Actually Slow

  1. Install React DevTools (Chrome/Firefox extension)
  2. Open Profiler tab → click Record → interact with your app → Stop
  3. Click a component bar → check "Why did this render?"
  4. Look for: "Props changed" with identical-looking values → unstable references
tsx
// Quick diagnostic: log whenever effect runs useEffect(() => { console.log('Effect fired. userId:', userId); // If this logs on every keystroke unrelated to userId, // you have an unstable reference in the dependency array }, [userId]);

Decision Chart

SituationTool
Function in useEffect depsuseCallback or move function inside effect
Expensive derived valueuseMemo
Child component re-renders too oftenReact.memo + useCallback for prop callbacks
Custom hook returning functionsuseCallback on returned functions
React 19 + React Compiler enabledNothing, compiler handles it

What NOT to Memoize

  • Simple inline event handlers (onClick={() => setCount(c => c + 1)})
  • Functions that aren’t in dependency arrays or passed to memoized children
  • Everything preemptively: measure first, optimize second

The rule: memoize to solve a specific, measured problem, not as a default coding style.

Further Reading

X / Twitter
LinkedIn
Facebook
WhatsApp
Telegram
AI Engineering for B2B

Stuck between an AI pilot and a system your team can run?

I join your engineering team and build the agent layer alongside you, covering architecture, MCP integration, evals, and production deployment. When the engagement ends, your team owns the system and keeps shipping.

12+ years shipping production systems

Senior engineer turned AI specialist. React, Next.js, AWS, agent orchestration.

Dubai-based, working with B2B teams worldwide

Direct collaboration across UAE, Europe, and US time zones.

AI agent teams that ship, not demos that stall

Discovery, role design, MCP integration, evals, and production deployment.

Questions about this piece

Follow-ups readers ask most often about the argument above.

  • Using useCallback around functions prevents them from being recreated on every render. When a fresh function ends up in the dependency array of useEffect, React treats it as a changed dependency and re-runs the effect every render, even if the logic has not changed. Memoizing stabilizes the reference.

  • useCallback memoizes a function (returns the same function reference across renders). useMemo memoizes the return value of a function (avoids re-computing expensive values). Rule of thumb: useCallback for stable function references; useMemo for expensive derived data.

  • No. useCallback adds memory and garbage collection overhead. It pays off when: (1) the function is in a useEffect dependency array, (2) it's passed as a prop to a React.memo component, or (3) it's returned from a custom hook. Avoid wrapping every function indiscriminately.

  • React 19 introduced the React Compiler (formerly React Forget), which automatically memoizes components and hook values. It eliminates the need for manual useCallback and useMemo in most cases when enabled. The use() hook also lets you consume promises and context more ergonomically inside render.

  • Use React DevTools Profiler: record an interaction, click a component, and inspect the 'Why did this render?' panel. For effects, add a console.log inside useEffect during development. If it logs on every keystroke, your dependencies include an unstable reference.

Get practical AI and engineering playbooks

Weekly field notes on private AI, automation, and high-performance Next.js builds. Each edition is concise, implementation-ready, and tested in production work.

Open full subscription page

Get the latest insights on AI and full-stack development.