React Lifecycle 2026: Hooks + Concurrent Rendering 🚀
React 19 lifecycle combines traditional useEffect phases (mount/update/unmount) with concurrent rendering, Server Components hydration, Suspense boundaries, and useOptimistic updates. Modern apps use useLayoutEffect for sync DOM measurements, useInsertionEffect for CSS-in-JS, useTransition for non-urgent updates, and automatic cleanup to prevent memory leaks.
🎯 React 19 Lifecycle Phases
| Phase | Class Method | Hook Equivalent | When It Runs |
|---|---|---|---|
| Mount | componentDidMount | useEffect(fn, []) | After paint |
| Layout | useLayoutEffect(fn, []) | Before paint | DOM measurements |
| Update | componentDidUpdate | useEffect(fn, [deps]) | After paint |
| Cleanup | componentWillUnmount | useEffect(fn, []) → return | Before unmount |
| Concurrent | N/A | useTransition() | Non-urgent updates |
🏗️ Complete Lifecycle Implementation
1. Mounting Phase (Initial Render)
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// Mounting: Runs ONCE after first paint
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(result => {
if (!cancelled) {
setUser(result);
setLoading(false);
}
});
return () => { cancelled = true; }; // Cleanup
}, [userId]);
if (loading) return <Spinner />;
return <UserCard user={user!} />;
}
2. Layout Effects (Before Paint)
// Sync measurements before browser paint
function Tooltip({ children, targetRef }: Props) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// 🔥 useLayoutEffect: Sync, before paint
useLayoutEffect(() => {
const rect = targetRef.current?.getBoundingClientRect();
if (rect) {
setPosition({
x: rect.right + 8,
y: rect.top
});
}
}, []);
return (
<>
{children}
<div
className="tooltip"
style={{ left: position.x, top: position.y }}
/>
</>
);
}
3. Updating Phase (State/Props Change)
function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState([]);
// Runs whenever `query` changes
useEffect(() => {
let cancelled = false;
debounceSearch(query).then(newResults => {
if (!cancelled) setResults(newResults);
});
return () => { cancelled = true; };
}, [query]); // ✅ Dependency array
return <ResultList results={results} />;
}
🚀 Concurrent Features (React 19)
useTransition - Non-Urgent Updates
function TabPanel({ tabs, activeTab, onTabChange }: Props) {
const [isPending, startTransition] = useTransition();
const handleTabChange = (tabId: string) => {
// ✅ Urgent: Tab state updates immediately
onTabChange(tabId);
// ❌ Non-urgent: Filter 10k results
startTransition(() => {
setFilteredResults(filterResults(tabId));
});
};
return (
<>
<TabHeaders activeTab={activeTab} onChange={handleTabChange} />
{isPending ? <Spinner /> : <ResultsList results={filteredResults} />}
</>
);
}
Suspense + Streaming
// Server Component with Suspense boundary
function Dashboard() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<UserProfile />
<RecentActivity />
<AnalyticsChart />
</Suspense>
);
}
🛠️ Custom Lifecycle Hooks
useMount (Only Once)
function useMount(effect: () => void | (() => void)) {
useEffect(effect, []);
}
// Usage
useMount(() => {
analytics.track('page_view');
return () => {
window.removeEventListener('scroll', handleScroll);
};
});
useUpdate (Skip Initial Mount)
function useUpdate(effect: () => void, deps: any[]) {
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
return effect();
}
}, deps);
}
// Usage
useUpdate(() => {
saveToLocalStorage(formData);
}, [formData]);
usePrevious (Track Previous Values)
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
const prevCount = usePrevious(count);
if (prevCount !== undefined) {
console.log(`Count changed: ${prevCount} → ${count}`);
}
⚠️ Cleanup Best Practices
// ✅ Proper cleanup prevents memory leaks
useEffect(() => {
const timer = setInterval(fetchData, 5000);
const resizeHandler = () => updateLayout();
window.addEventListener('resize', resizeHandler);
return () => {
clearInterval(timer);
window.removeEventListener('resize', resizeHandler);
};
}, []);
// ❌ Missing cleanup = memory leaks
useEffect(() => {
const timer = setInterval(fetchData, 5000);
// Missing: clearInterval(timer);
}, []);
📊 Effect Splitting (Single Responsibility)
// ❌ God effect (hard to debug)
useEffect(() => {
fetchData();
setupEventListeners();
saveToStorage();
}, [id]);
// ✅ Split by responsibility
useEffect(() => {
fetchData();
}, [id]);
useEffect(() => {
const handleResize = () => updateLayout();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
saveToStorage(data);
}, [data]);
🎯 Server Components + Client Lifecycle
// app/dashboard/page.tsx (Server Component)
async function DashboardPage() {
const initialData = await fetchDashboardData();
return (
<ClientDashboard initialData={initialData} />
);
}
// Client Component lifecycle
'use client';
function ClientDashboard({ initialData }: { initialData: DashboardData }) {
const [data, setData] = useState(initialData);
// Client-side hydration complete
useEffect(() => {
// Start real-time updates
const socket = new WebSocket('/ws/dashboard');
socket.onmessage = (event) => {
setData(JSON.parse(event.data));
};
return () => socket.close();
}, []);
return <DashboardView data={data} />;
}
🚀 Production Checklist
✅ [] useEffect cleanup prevents leaks ✅ [] useLayoutEffect for sync measurements ✅ [] useTransition for non-urgent updates ✅ [] Split effects by responsibility ✅ [] ESLint exhaustive-deps enabled ✅ [] Custom hooks for reusable lifecycle ✅ [] Server Components pass initial data ✅ [] Suspense boundaries for streaming ✅ [] useOptimistic for instant feedback
🔥 Common Anti-Patterns
// ❌ Empty deps = stale closures
const userId = props.userId;
useEffect(() => {
fetchData(userId); // Stale userId!
}, []);
// ✅ Fix: Include all dependencies
useEffect(() => {
fetchData(props.userId);
}, [props.userId]);
// ✅ OR useCallback
const fetchData = useCallback((id: string) => {
// ...
}, []);
🎯 Lifecycle Visual Map
INITIAL RENDER: ├── Render JSX ├── Commit to DOM └── useLayoutEffect (sync) └── useEffect (async)
STATE/PROPS CHANGE: ├── Render JSX ├── Commit to DOM ├── useLayoutEffect └── useEffect
UNMOUNT: └── useEffect cleanup → componentWillUnmount
🎯 Final Thoughts
Modern React lifecycle = Predictable + Concurrent. useEffect handles async side effects, useLayoutEffect sync measurements, useTransition non-urgent updates, and Server Components eliminate client-side waterfalls.
2026 React workflow: Server Components → Initial render
useLayoutEffect → Measurements
useEffect → Async data
useTransition → Background tasks
Cleanup → Memory safety
Master lifecycle → 60fps apps. Build production React with concurrent features and optimal performance 🚀.