React.js Intermediate 14 min read

React Lifecycle 2026: Hooks + Concurrent Rendering 🚀

Complete React 19 lifecycle guide covering useEffect patterns, useLayoutEffect, useInsertionEffect, concurrent features, Server Components integration, Suspense boundaries, and cleanup best practices.

#react lifecycle #useEffect #useLayoutEffect #concurrent rendering #server components
Guide React.js

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

PhaseClass MethodHook EquivalentWhen It Runs
MountcomponentDidMountuseEffect(fn, [])After paint
LayoutuseLayoutEffect(fn, [])Before paintDOM measurements
UpdatecomponentDidUpdateuseEffect(fn, [deps])After paint
CleanupcomponentWillUnmountuseEffect(fn, [])returnBefore unmount
ConcurrentN/AuseTransition()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 🚀.

Chat with us