惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
博客园 - 聂微东
罗磊的独立博客
W
WeLiveSecurity
博客园_首页
Scott Helme
Scott Helme
V
Visual Studio Blog
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
About on SuperTechFans
F
Full Disclosure
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 司徒正美
博客园 - Franky
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
阮一峰的网络日志
阮一峰的网络日志
S
Schneier on Security
雷峰网
雷峰网
博客园 - 【当耐特】
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
Engineering at Meta
Engineering at Meta
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
T
Tor Project blog
V
V2EX
爱范儿
爱范儿
C
Check Point Blog
T
Threatpost
Project Zero
Project Zero
量子位
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
I
Intezer
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Back to React Fundamentals: useEffect, useState, useMemo, useCallback, useReducer
kensaadi · 2026-05-10 · via DEV Community

Updated for React 19.2 (2025)

Over the last few years, the React ecosystem has become increasingly complex.

Server Components, compilers, signals, state managers, meta-frameworks, cache layers, streaming, optimistic UI.

But while everyone is chasing the next big thing, many real-world React problems still come from the same places:

  • poorly used useEffect
  • misunderstood dependencies
  • unnecessary re-renders
  • premature memoization
  • oversized components
  • badly distributed state

This article is a return to fundamentals.

Not theoretical concepts, but the things that truly matter when building scalable and maintainable React applications.


Latest: React 19.2 Release (October 2025)

Concurrent rendering was introduced in React 18 via createRoot, not React 19.

React 19.2 adds several important features:

  • useEffectEvent — Separate reactive from non-reactive logic in effects
  • <Activity /> — Hide and restore UI sections with independent state management
  • cacheSignal — Know when server component cache lifetime is over (RSCs only)
  • Performance Tracks — New Chrome DevTools performance profiling
  • Partial Pre-rendering — Pre-render static parts and resume with dynamic content later

Key insight: These features build on the fundamentals below. Master state colocation, dependency management, and component splitting first—then layer in React 19's advanced capabilities.


First rule: React renders functions

When a React component updates:

  • the component function runs again
  • all hooks run again
  • objects and functions declared inside are recreated
  • React performs reconciliation to identify what actually changed

Example:

function Counter() {
  console.log('Counter render');

  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

Enter fullscreen mode Exit fullscreen mode

Every click:

  • re-executes Counter
  • recreates the onClick function
  • reevaluates the JSX

This is completely normal.

The real problem starts when:

  • the component becomes huge
  • it contains too much logic
  • it renders expensive children
  • it passes unstable props
  • it centralizes too much state

And this leads us to the first truly important concept.

Splitting code into components

One of the most powerful ways to optimize React is not useMemo.

It's splitting components correctly.

The most common mistake is putting too much state and UI inside the same component.

Problematic example

function Dashboard() {
  const [count, setCount] = useState(0);

  console.log('Dashboard render');

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increment: {count}
      </button>

      <ExpensiveChart />
      <ComplexTable />
      <HeavySidebar />
    </div>
  );
}

function ExpensiveChart() {
  console.log('ExpensiveChart render');

  return <div>Chart</div>;
}

function ComplexTable() {
  console.log('ComplexTable render');

  return <div>Table</div>;
}

function HeavySidebar() {
  console.log('HeavySidebar render');

  return <div>Sidebar</div>;
}

Enter fullscreen mode Exit fullscreen mode

Every click updates the state of Dashboard.

As a consequence:

  • Dashboard re-runs
  • JSX is recreated
  • child React elements are recreated
  • React reevaluates the child tree

So you might see:

  • Dashboard render
  • ExpensiveChart render
  • ComplexTable render
  • HeavySidebar render

even though those components do not depend on count.

The issue is not the button.

The issue is that the state lives too high in the tree.

Solution: move state closer to where it's actually needed

function Dashboard() {
  console.log('Dashboard render');

  return (
    <div>
      <CounterSection />

      <ExpensiveChart />
      <ComplexTable />
      <HeavySidebar />
    </div>
  );
}

function CounterSection() {
  const [count, setCount] = useState(0);

  console.log('CounterSection render');

  return (
    <button onClick={() => setCount(count + 1)}>
      Increment: {count}
    </button>
  );
}

function ExpensiveChart() {
  console.log('ExpensiveChart render');

  return <div>Chart</div>;
}

function ComplexTable() {
  console.log('ComplexTable render');

  return <div>Table</div>;
}

function HeavySidebar() {
  console.log('HeavySidebar render');

  return <div>Sidebar</div>;
}

Enter fullscreen mode Exit fullscreen mode

Now when you click the button:
render

This is the crucial difference:

A child state update does not automatically force parent components to re-render.

React starts the update from the component whose state changed and only re-renders the necessary part of the tree.

Before refactoring, the state lived in Dashboard, so every click forced Dashboard to run again and recreate the child tree. All children would be re-evaluated as a result.

After refactoring, the state lives inside CounterSection, so the update stays local. The state update triggers a re-render of CounterSection only.

Because Dashboard no longer updates, React has no reason to traverse and re-render those children. ExpensiveChart, ComplexTable, and HeavySidebar are not affected by this state change.

This is the core principle: state that affects only a small part of the UI should live in that part. Where state lives determines where reconciliation starts and what gets re-rendered.

This is the real value of component splitting

You didn't use:

  • useMemo
  • useCallback
  • React.memo

You simply:

  • isolated responsibilities
  • localized state
  • reduced update propagation

Note on React.memo:

React.memo has legitimate uses—wrapping expensive components that receive stable props, or protecting components that receive object/function props. But it's a second-line optimization. Component splitting solves 90% of performance problems without it.

This is one of the most important React optimizations:

State colocation

Keeping state as close as possible to where it is used.

Practical rule

If a piece of state only belongs to a small part of the UI, don't put it in the parent component.

Better:

function Page() {
  return (
    <>
      <Header />
      <SearchBox />
      <Results />
    </>
  );
}

function SearchBox() {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={(event) => setQuery(event.target.value)}
    />
  );
}

Enter fullscreen mode Exit fullscreen mode

Worse:

function Page() {
  const [query, setQuery] = useState('');

  return (
    <>
      <Header />
      <SearchBox
        query={query}
        setQuery={setQuery}
      />
      <Results />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

In the second example, every keystroke may rerun the entire Page.

In the first one, the update stays local.

Component splitting vs Lazy Loading

These two concepts are often confused.

But they solve completely different problems.

Component Splitting

Used for:

  • isolating responsibilities
  • limiting state propagation
  • improving maintainability
  • making renders more predictable

Lazy Loading

Used for:

  • loading code only when needed
  • reducing the initial bundle size
  • improving initial loading performance

Example:

const AdminPage = lazy(() => import('./AdminPage'));

Enter fullscreen mode Exit fullscreen mode

This does NOT optimize renders.

It optimizes bundle downloading.

useState: values vs references

Before truly understanding useEffect, you need to understand something fundamental:

in JavaScript there are:

  • primitive values
  • references

Primitive values

const a = 5;
const b = 5;

console.log(a === b); // true

Enter fullscreen mode Exit fullscreen mode

Objects and arrays

const a = { name: 'Ken' };
const b = { name: 'Ken' };

console.log(a === b); // false

Enter fullscreen mode Exit fullscreen mode

Why?

Because JavaScript compares memory references.

Not contents.

This concept is ESSENTIAL for understanding:

  • useEffect
  • useMemo
  • useCallback
  • React.memo
  • re-renders

useEffect: it's not really componentDidMount

Many developers still think of useEffect as:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

It's a useful beginner mental model.

But it's not what useEffect truly is.

useEffect exists to synchronize React with the outside world.

Examples:

  • APIs
  • timers
  • websockets
  • localStorage
  • browser events
  • analytics

useEffect equivalent to didMount

useEffect(() => {
  console.log('mounted');
}, []);

Enter fullscreen mode Exit fullscreen mode

The empty dependency array means:

run the effect only on mount.

Real-world example: API call on mount

For fetch-on-mount scenarios, the nested function pattern is the modern best practice:

function UsersPage() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    async function loadUsers() {
      const response = await fetch('/api/users');

      const data = await response.json();

      setUsers(data);
    }

    loadUsers();
  }, []);

  return <UsersTable users={users} />;
}

Enter fullscreen mode Exit fullscreen mode

Why this pattern?

  • The function is defined inside useEffect, so it doesn't need to be a dependency
  • No memoization overhead
  • Simpler and more readable
  • The function is isolated to where it's actually used

When you DO need useCallback with useEffect

Only reach for useCallback when loadUsers needs to be called elsewhere or exposed as a stable reference:

function UsersPage() {
  const [users, setUsers] = useState([]);

  const loadUsers = useCallback(async () => {
    const response = await fetch('/api/users');

    const data = await response.json();

    setUsers(data);
  }, []);

  // ✅ Use useCallback here because:
  // - loadUsers is a dependency of useEffect
  // - loadUsers is passed as a prop to child components
  // - loadUsers might be called from multiple places

  useEffect(() => {
    loadUsers();
  }, [loadUsers]);

  return (
    <div>
      <RefreshButton onRefresh={loadUsers} />
      <UsersTable users={users} />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

The key difference:

  • Nested function: Simpler, no memoization. Use when the function only belongs inside useEffect.
  • useCallback: Overhead, but necessary when the function is a dependency or needs to be stable across renders.

Default approach: Start with the nested function. Only add useCallback if you actually need a stable reference.

Why you should NOT use async directly in useEffect

Common mistake:

useEffect(async () => {
  // NO
}, []);

Enter fullscreen mode Exit fullscreen mode

Because useEffect expects:

  • void
  • or a cleanup function

An async function always returns a Promise.

Cleanup function

useEffect(() => {
  const interval = setInterval(() => {
    console.log('tick');
  }, 1000);

  return () => {
    clearInterval(interval);
  };
}, []);

Enter fullscreen mode Exit fullscreen mode

The cleanup function runs:

  • on unmount
  • before the next effect execution

useEffect as didUpdate

Single dependency

useEffect(() => {
  console.log('count changed');
}, [count]);

Enter fullscreen mode Exit fullscreen mode

The effect runs when count changes.

Multiple dependencies

useEffect(() => {
  console.log('filters changed');
}, [search, page, sort]);

Enter fullscreen mode Exit fullscreen mode

It runs whenever at least one dependency changes.

Object dependencies

This is where many real-world problems begin.

const filters = {
  search,
  page,
};

useEffect(() => {
  console.log('filters changed');
}, [filters]);

Enter fullscreen mode Exit fullscreen mode

This effect will run EVERY render.

Why?

Because the object is recreated every render.

Solution with useMemo

const filters = useMemo(() => {
  return {
    search,
    page,
  };
}, [search, page]);

Enter fullscreen mode Exit fullscreen mode

Now the reference stays stable.

useMemo: when to actually use it

useMemo is NOT for:

  • "optimizing React"
  • "avoiding renders"
  • "making everything faster"

It's for memoizing expensive values or stabilizing references.

Important: useMemo is a performance optimization hint, not a semantic guarantee. React may discard memoized values in some scenarios, especially during development, concurrent rendering, or future internal optimizations. Never depend on memoization for correctness—only for performance.

Correct use case

const sortedUsers = useMemo(() => {
  return [...users].sort((a, b) =>
    a.name.localeCompare(b.name)
  );
}, [users]);

Enter fullscreen mode Exit fullscreen mode

This makes sense because:

  • sorting may be expensive
  • the result gets reused

WRONG use case

const fullName = useMemo(() => {
  return `${name} ${surname}`;
}, [name, surname]);

Enter fullscreen mode Exit fullscreen mode

The cost here is irrelevant.

You added unnecessary complexity.

The problem with memoizing everything

Many modern React projects look like this:

useMemo(...)
useCallback(...)
useMemo(...)
useCallback(...)

Enter fullscreen mode Exit fullscreen mode

everywhere.

This often worsens:

  • readability
  • debugging
  • maintainability

And introduces mental overhead.

Important truth

If your code is properly split:

most of the time you do NOT need useMemo.

useCallback

useCallback memoizes a function.

const handleClick = useCallback(() => {
  console.log('click');
}, []);

Enter fullscreen mode Exit fullscreen mode

It is mainly useful when:

  • passing callbacks to memoized children
  • reference stability matters
  • the function is a dependency of an effect

The real difference between <Component /> and {component}

This is one of the least understood parts of React.

Many developers think:

<Child />

Enter fullscreen mode Exit fullscreen mode

and:

{child}

Enter fullscreen mode Exit fullscreen mode

are equivalent.

They are not.

The difference depends on WHERE the React element is created.

Case 1 — <Child /> inside render

function Child() {
  console.log('Child render');

  return <div>Child</div>;
}

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      <Child />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Every update of Parent:

  • reruns Parent
  • recreates <Child />
  • React reevaluates the child

So:

Child render

appears on every click.

Case 2 — React element stabilized outside the component

const child = <Child />;

function Child() {
  console.log('Child render');

  return <div>Child</div>;
}

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>

      {child}
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Here:

  • child is created only once
  • the reference stays stable
  • React always receives the same element

So Child does NOT rerun every parent render.

Case 3 — Variable inside render

Many developers do this:

function Parent() {
  const child = <Child />;

  return <div>{child}</div>;
}

Enter fullscreen mode Exit fullscreen mode

But this changes NOTHING compared to:

<Child />

Enter fullscreen mode Exit fullscreen mode

Because:

const child = <Child />;

Enter fullscreen mode Exit fullscreen mode

runs again every render.

So:

  • new React element
  • new reference
  • new reconciliation

React element vs Component Function

This is the key concept.

This:

<Child />

Enter fullscreen mode Exit fullscreen mode

is NOT the component itself.

It's a React element.

An object similar to:

{
  type: Child,
  props: {}
}

Enter fullscreen mode Exit fullscreen mode

When React receives a new element:

  • it decides whether to render
  • compares the tree
  • performs reconciliation

Very common pattern: passing React elements as props

Many modern UI systems use this pattern:

function Layout({ sidebar }) {
  return (
    <div>
      <aside>{sidebar}</aside>
      <main>Content</main>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

Usage:

const sidebar = <Sidebar />;

function App() {
  return <Layout sidebar={sidebar} />;
}

Enter fullscreen mode Exit fullscreen mode

Here sidebar remains stable.

If App rerenders:

  • React receives the same React element
  • Sidebar may avoid unnecessary rerenders

But be careful

Do NOT overuse this pattern.

Because it may:

  • hurt readability
  • create unexpected behavior
  • make the component tree less natural

Stabilizing React elements outside render is an advanced optimization pattern. In most applications, proper component splitting and state colocation are significantly more important.

Most of the time:

<Sidebar />

Enter fullscreen mode Exit fullscreen mode

is perfectly fine.

The real React optimization

Modern React optimization is about:

  • small components
  • localized state
  • stable props
  • simple architecture

Not:

  • useMemo everywhere
  • or const child = <Child /> everywhere

useReducer

useReducer is one of the most underrated React hooks.

It is NOT Redux

Many developers associate it with Redux.

But useReducer is simply:

  • local state management
  • reducer-based logic
  • excellent for complex flows

Basic syntax

const initialState = {
  count: 0,
};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        count: state.count + 1,
      };

    case 'decrement':
      return {
        ...state,
        count: state.count - 1,
      };

    default:
      return state;
  }
}

Enter fullscreen mode Exit fullscreen mode

Usage:

const [state, dispatch] = useReducer(
  reducer,
  initialState
);

Enter fullscreen mode Exit fullscreen mode

Dispatch

dispatch({ type: 'increment' });

Enter fullscreen mode Exit fullscreen mode

When to prefer useReducer

useState is perfect for simple state:

const [open, setOpen] = useState(false);

Enter fullscreen mode Exit fullscreen mode

But when you start having:

  • multiple related states
  • complex transitions
  • dependent updates
  • multi-step flows

useReducer becomes much clearer.

Real-world example: fetch state

const initialState = {
  loading: false,
  error: null,
  data: null,
};

Enter fullscreen mode Exit fullscreen mode

Reducer:

function reducer(state, action) {
  switch (action.type) {
    case 'fetch_start':
      return {
        ...state,
        loading: true,
      };

    case 'fetch_success':
      return {
        loading: false,
        data: action.payload,
        error: null,
      };

    case 'fetch_error':
      return {
        loading: false,
        data: null,
        error: action.payload,
      };

    default:
      return state;
  }
}

Enter fullscreen mode Exit fullscreen mode

Here useReducer makes the flow:

  • more readable
  • more predictable
  • more scalable

React 19 & Beyond: Building on Fundamentals

React 19 introduces powerful new features. React 19.2 adds more. But they all build on the fundamentals covered above.

Server Actions (React 19)

Server Actions simplify async workflows through functions marked with 'use server':

'use server';

async function updateUser(formData) {
  const result = await db.users.update(formData);
  revalidatePath('/users');
  return result;
}

Enter fullscreen mode Exit fullscreen mode

Used in a component:

function UserForm() {
  const [isPending, startTransition] = useTransition();

  return (
    <form action={updateUser}>
      <input name="email" required />
      <button type="submit" disabled={isPending}>
        Save
      </button>
    </form>
  );
}

Enter fullscreen mode Exit fullscreen mode

Why fundamentals apply: Even with Server Actions, you're managing state colocation (isPending stays local), understanding dependencies, and avoiding unnecessary re-renders (React handles submission state automatically).

useActionState & useOptimistic (React 19)

React 19 introduces useActionState for form submissions and useOptimistic for optimistic updates:

function Todo() {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(todos);

  const [formState, formAction] = useActionState(addTodoAction);

  return (
    <form action={async (formData) => {
      addOptimisticTodo(formData.get('todo'));
      await formAction(formData);
    }}>
      <input name="todo" />
      <button type="submit">Add</button>
    </form>
  );
}

Enter fullscreen mode Exit fullscreen mode

Why fundamentals apply: These APIs handle the complex state management patterns you'd normally need useReducer or useState for—but they're built on the same dependency and state management principles.

useEffectEvent (React 19.2)

useEffectEvent separates reactive from non-reactive logic in effects, formalizing a core concept:

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ theme is NOT a dependency
}

Enter fullscreen mode Exit fullscreen mode

Why fundamentals apply: This hook encodes a principle you already understand—effects depend on specific values (roomId matters), and non-reactive logic shouldn't trigger re-effects. Dependencies matter; this hook just makes it explicit.

<Activity /> (React 19.2)

The Activity component allows sections of the UI tree to be paused, hidden, and resumed independently:

<Activity mode={isVisible ? 'visible' : 'hidden'}>
  <Page />
</Activity>

Enter fullscreen mode Exit fullscreen mode

When hidden, Activity unmounts effects and defers updates. This pre-renders hidden sections without impacting visible performance.

Why fundamentals apply: Activity enables fine-grained control over which parts of the tree are active, following the same principle as state colocation—isolating state and rendering to where it matters.

Conclusion

Most modern React problems are not solved with:

  • more libraries
  • more abstractions
  • more custom hooks
  • more memoization

They are solved by returning to fundamentals.

Truly understanding:

  • when React renders
  • what changes by reference
  • how dependencies work
  • when to split components
  • when NOT to optimize
  • what referential stability means

is far more valuable than premature micro-optimizations.

Because most of the time, the best React performance does not come from useMemo.

It comes from simple architecture.


Remember: The best React developers are usually not the ones using the most hooks.

They are the ones building the simplest render trees.


Article Version: 2.1 (Updated October 2025 for React 19.2, with technical precision)