<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sachintha</title>
    <description>The latest articles on DEV Community by Sachintha (@sxchintha).</description>
    <link>https://dev.to/sxchintha</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3621900%2Fe6926922-44a1-4b4d-92bb-6f509548616c.png</url>
      <title>DEV Community: Sachintha</title>
      <link>https://dev.to/sxchintha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sxchintha"/>
    <language>en</language>
    <item>
      <title>Should you proxy through Server Actions or call APIs directly?</title>
      <dc:creator>Sachintha</dc:creator>
      <pubDate>Fri, 13 Nov 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/sxchintha/should-you-proxy-through-server-actions-or-call-apis-directly-pkm</link>
      <guid>https://dev.to/sxchintha/should-you-proxy-through-server-actions-or-call-apis-directly-pkm</guid>
      <description>&lt;p&gt;You've got a Next.js app, and it needs to talk to a backend that isn't Next.js, maybe a Python service, a Rails app, or plain Express. The first architectural decision you'll make, often without realizing it's a decision at all, is where that connection actually lives.&lt;/p&gt;

&lt;p&gt;Some teams route every request through a Server Action and let Next.js sit in the middle. Others skip that step and have the browser call the backend directly. Both are common, both work, and picking wrong doesn't blow up right away. It just shows up later as latency nobody budgeted for, or a security gap nobody planned for.&lt;/p&gt;

&lt;p&gt;This post walks through how each pattern works, then digs into the real trade-offs around performance, scaling, and security, so you can pick deliberately instead of by habit. It's written for developers who've built a Next.js app or two. You don't need to have used Server Actions before, the next section explains what they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are two places the connection can live
&lt;/h2&gt;

&lt;p&gt;The connection to your backend either runs through the Next.js server or skips it. A Server Action is an async function marked with &lt;code&gt;'use server'&lt;/code&gt; that runs on the Next.js server, but that a client component can call like a normal function. That makes it a natural middleman.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pattern A: Server Actions as a proxy.&lt;/strong&gt; The Next.js app becomes a middleman. Client components call Server Actions, which forward requests to the backend. This is the Backend-for-Frontend (BFF) approach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern B: direct client-side calls.&lt;/strong&gt; The browser talks straight to the backend, the same way client-side rendering has always worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how a request travels in each.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proxy adds a hop, a direct call skips it
&lt;/h2&gt;

&lt;p&gt;The proxy pattern sends every request through the Next.js server on its way to the backend, while a direct call goes straight from the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server Actions as a proxy
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A user action in the app triggers a Server Action&lt;/li&gt;
&lt;li&gt;The Server Action runs on the Next.js server and calls the backend API&lt;/li&gt;
&lt;li&gt;The backend responds to the Next.js server&lt;/li&gt;
&lt;li&gt;The Server Action processes that response and sends it to the client&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The path: client, Next.js server, backend API, Next.js server, client.&lt;/p&gt;

&lt;p&gt;Under the hood, each Server Action becomes a POST endpoint on the page that uses it, identified by an action ID Next.js generates. Called from the client, it always goes over POST, regardless of what the action actually does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct client-side calls
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A client component needs data&lt;/li&gt;
&lt;li&gt;Your JavaScript calls the backend directly with fetch or Axios&lt;/li&gt;
&lt;li&gt;The backend responds straight to the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Next.js server serves the initial JavaScript bundle, then steps out of the way. Every request after that happens in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each pattern wins on different things
&lt;/h2&gt;

&lt;p&gt;Neither approach is better across the board. What matters is what you're optimizing for.&lt;/p&gt;

&lt;h3&gt;
  
  
  When Server Actions make sense
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Secrets stay hidden.&lt;/strong&gt; API keys and service tokens live on the server and never reach the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth lives in one place.&lt;/strong&gt; Every request can be validated against the user's session before it reaches the backend, instead of trusting each client call independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client code stays simple.&lt;/strong&gt; Components call functions instead of managing fetch calls, error states, and loading states by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache invalidation is built in.&lt;/strong&gt; After a mutation, calling &lt;code&gt;revalidatePath&lt;/code&gt; or &lt;code&gt;updateTag&lt;/code&gt; updates the UI in the same response. No manual cache bookkeeping.&lt;/p&gt;

&lt;h3&gt;
  
  
  When direct calls make more sense
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Your backend already does the work.&lt;/strong&gt; If the API already handles OAuth, JWTs, rate limiting, and CORS, a proxy layer adds a hop without adding much value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency is the priority.&lt;/strong&gt; Direct calls skip the extra round trip through the Next.js server, which matters for real-time features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want a thin frontend.&lt;/strong&gt; If Next.js should just serve pages, direct calls keep it out of the request path entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer experience, side by side
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you care about&lt;/th&gt;
&lt;th&gt;Server Action proxy&lt;/th&gt;
&lt;th&gt;Direct client calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client-side code&lt;/td&gt;
&lt;td&gt;Function calls&lt;/td&gt;
&lt;td&gt;Manual fetch setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Typed at the call site, manual past the backend boundary&lt;/td&gt;
&lt;td&gt;Manual type definitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate&lt;/td&gt;
&lt;td&gt;Minimal, hooks manage state&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood/" rel="noopener noreferrer"&gt;useState&lt;/a&gt; + &lt;a href="https://blog.sachinthagunaratne.com/posts/how-useeffect-works-behind-the-scenes-in-react/" rel="noopener noreferrer"&gt;useEffect&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache invalidation&lt;/td&gt;
&lt;td&gt;Built in with revalidatePath&lt;/td&gt;
&lt;td&gt;Custom solution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That type safety row needs a caveat. "End-to-end, automatic" is the pitch when the whole stack is TypeScript, tRPC being the clearest example. The moment your backend is Python, Rails, or anything else, the automation stops at the Next.js boundary. The Server Action's own inputs and outputs are typed, but whatever the backend actually sends back still needs validating by hand, with something like Zod, unless you're maintaining matching types on both sides yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server Actions cost a hop and run one at a time
&lt;/h2&gt;

&lt;p&gt;Server Actions add a network hop. There's no getting around that.&lt;/p&gt;

&lt;p&gt;On serverless platforms like Vercel, that hop can push latency into the hundreds of milliseconds to low seconds, worse with cold starts. Direct calls go point to point, so they're faster by default.&lt;/p&gt;

&lt;p&gt;That number is more a serverless characteristic than a Server Actions one. Run Next.js as a long-lived process next to your backend, same region, nothing scaling to zero, and the extra hop can cost single-digit milliseconds instead. If your own latency numbers look like the Vercel figures above, check what's actually adding the delay before blaming the pattern itself.&lt;/p&gt;

&lt;p&gt;Here's a quick test. Fire three Server Actions from the client with &lt;code&gt;Promise.all&lt;/code&gt;, each taking about a second on the server. How long until all three finish? Most people guess one second. It's closer to three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Actions run one at a time.&lt;/strong&gt; Trigger several at once and they queue up while the UI waits. The &lt;a href="https://react.dev/reference/rsc/use-server" rel="noopener noreferrer"&gt;React docs&lt;/a&gt; say frameworks typically process one action at a time, and Next.js does. Direct calls with &lt;code&gt;Promise.all&lt;/code&gt; really do run concurrently.&lt;/p&gt;

&lt;p&gt;Don't use Server Actions for fetching data. Use Server Components or API routes with GET requests instead, Server Actions are built for mutations, and using them for reads gives up caching behavior you'd otherwise get for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proxy centralizes load as well as logic
&lt;/h2&gt;

&lt;p&gt;Centralizing data logic behind Server Actions is easier to maintain. One pattern, one place to update. The cost is that all that load lands on the Next.js server. As traffic grows, that server needs to scale with it, and on pay-per-invocation platforms the bill grows with it too.&lt;/p&gt;

&lt;p&gt;Direct calls let frontend and backend scale independently. The Next.js layer stays thin and the backend absorbs its own load. The cost shows up later: fetching logic spreads across components, and tracing what calls what gets harder as the app grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proxy hides more, but it's still attack surface
&lt;/h2&gt;

&lt;p&gt;The proxy pattern is architecturally stronger on security, for a few concrete reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys and tokens never touch the browser&lt;/li&gt;
&lt;li&gt;The backend doesn't need to be publicly exposed, it can be firewalled to accept requests only from the Next.js server&lt;/li&gt;
&lt;li&gt;CSRF protection comes built in&lt;/li&gt;
&lt;li&gt;Authorization checks happen in one place, before anything reaches the backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Going direct puts more of that burden on the backend itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWTs stored in the browser are a target. Store them wrong and an XSS attack can steal them.&lt;/li&gt;
&lt;li&gt;CORS has to be configured correctly, which is easy to get wrong.&lt;/li&gt;
&lt;li&gt;The API needs its own rate limiting, input validation, and abuse protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is unworkable, it's just work the backend now has to do on its own.&lt;/p&gt;

&lt;p&gt;Neither pattern is immune to real vulnerabilities. &lt;a href="https://www.cve.org/CVERecord?id=CVE-2025-55182" rel="noopener noreferrer"&gt;CVE-2025-55182&lt;/a&gt;, the "React2Shell" vulnerability, allowed remote code execution via crafted payloads sent through the same POST/action-dispatch mechanism Server Actions use. The bug lived in React's own Flight protocol deserialization, not in anyone's Server Action code, but a proxy architecture still means treating Server Actions as endpoints that need patching and secure coding practices. The Next.js server can be the point of compromise too.&lt;/p&gt;

&lt;p&gt;That's worth sitting with, because it cuts against the "proxy is just safer" framing above. Every layer between the browser and your data is also a layer that can be exploited, and a full Next.js server running action-dispatch logic is a lot more code than a stateless API behind tight CORS and short-lived tokens. The proxy pattern reduces what the browser can reach directly. It doesn't reduce the total surface, it just moves where the risk lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick by what you're optimizing for
&lt;/h2&gt;

&lt;p&gt;Reach for Server Actions as a proxy when secrets need to stay hidden, when a centralized data access layer keeps the codebase easier to reason about, or when type-safe function calls matter more than shaving off a network hop.&lt;/p&gt;

&lt;p&gt;Reach for direct client calls when the backend already handles OAuth, rate limiting, and CORS on its own, when latency is the constraint you're optimizing for, or when Next.js should stay a thin layer that just serves pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most large apps use both
&lt;/h2&gt;

&lt;p&gt;Large applications often use both. Server Actions cover sensitive operations, payment processing, account settings, anything touching secrets. Direct calls handle public, cacheable data like blog posts or product listings that don't need protection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0j3b116jl186e5ig38i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0j3b116jl186e5ig38i.png" alt="A checkout request travels from the browser through a Next.js Server Action, which adds the API key, to the backend's payments endpoint in two hops, while a products request goes straight from the browser to the backend in one hop." width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Sensitive calls take the extra hop through a Server Action to keep secrets on the server, while public data goes direct.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That split gets you the security of a proxy where it matters and the speed of a direct call where protection isn't the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do Server Actions really only run over POST?
&lt;/h3&gt;

&lt;p&gt;Yes. Every Server Action, no matter what it does internally, gets invoked as a POST request. Next.js generates a hidden action ID and routes the call through that same mechanism, which is also part of why they don't get GET-style caching for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do Server Actions protect against CSRF?
&lt;/h3&gt;

&lt;p&gt;Two layers, by default. POST-only requests already sidestep most CSRF vectors, since SameSite cookies block cross-site POSTs in modern browsers. On top of that, Next.js compares the request's Origin header against the Host header and rejects the call if they don't match. You can widen that with the &lt;code&gt;allowedOrigins&lt;/code&gt; option in &lt;code&gt;next.config.js&lt;/code&gt; if you need cross-origin calls, but each origin you add is one more thing that has to be trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the actual difference between revalidatePath and revalidateTag?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;revalidatePath&lt;/code&gt; invalidates one specific route, and when a Server Action calls it, the response already carries the re-rendered page. &lt;code&gt;revalidateTag(tag, 'max')&lt;/code&gt; works by tag instead. It marks every piece of cached data with that tag as stale, wherever it's used, and serves the old version while fresh data loads in the background, so the user doesn't see their change right away. When they need to, call &lt;code&gt;updateTag(tag)&lt;/code&gt; in the Server Action instead, which expires the tag immediately. Reach for tags when the same data shows up in multiple places, and &lt;code&gt;revalidatePath&lt;/code&gt; when you only need to refresh one page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run several Server Actions in parallel with Promise.all?
&lt;/h3&gt;

&lt;p&gt;Not really, at least not the way you'd hope. Next.js's client sends Server Actions one at a time, waiting for each to finish before sending the next, and there's no setting to change that. The server itself doesn't queue them. If you need several things to happen at once, do the parallel work inside a single Server Action with &lt;code&gt;Promise.all&lt;/code&gt; there, or move the read side to a Server Component or route handler instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do Server Actions feel especially slow on Vercel?
&lt;/h3&gt;

&lt;p&gt;Serverless functions spin down when they're idle, and spinning one back up for the next request is what shows up as a cold start. That sits on top of the network hop a Server Action already adds. Keeping functions warm or moving latency-sensitive mutations closer to the edge helps, but it doesn't remove the extra hop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I ever use a Server Action to fetch data?
&lt;/h3&gt;

&lt;p&gt;Generally no. Server Actions are built for mutations, and fetching through one gives up the caching you'd get from a Server Component or a GET-based route handler. If a component just needs to read data, reach for those instead and save Server Actions for the writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you're optimizing for security and maintainability, the proxy pattern is the better default, even with the added latency. If you're optimizing for speed and a fully decoupled frontend, direct calls get you there, provided the backend can stand on its own as a public-facing API.&lt;/p&gt;

&lt;p&gt;Most real applications end up mixing both rather than picking one architecture for everything. The important part is making that choice deliberately, based on what a given feature actually needs, rather than copying whatever pattern the last tutorial used.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.sachinthagunaratne.com/posts/server-actions-proxy-vs-direct-calls/" rel="noopener noreferrer"&gt;Notes by Sachintha&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webperf</category>
    </item>
    <item>
      <title>How useEffect Works Behind the Scenes in React</title>
      <dc:creator>Sachintha</dc:creator>
      <pubDate>Fri, 21 Nov 2025 01:19:37 +0000</pubDate>
      <link>https://dev.to/sxchintha/how-useeffect-works-behind-the-scenes-in-react-3fjd</link>
      <guid>https://dev.to/sxchintha/how-useeffect-works-behind-the-scenes-in-react-3fjd</guid>
      <description>&lt;p&gt;You've probably called &lt;code&gt;useEffect&lt;/code&gt; hundreds of times and wrestled with its dependency array at least once. But what does React actually do when it hits that call? If you've read &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;how useState works under the hood&lt;/a&gt;, the fiber architecture here will look familiar, it's the same system powering both hooks.&lt;/p&gt;

&lt;p&gt;React doesn't just run your effect function whenever it feels like it. There's scheduling, cleanup, and a few optimizations behind the scenes that decide exactly when your code runs.&lt;/p&gt;

&lt;p&gt;So why does your effect run after the screen updates, and not the moment React reads it? This post answers that by walking through the Fiber architecture, the render and commit phases, and how React schedules and cleans up effects. If you've used &lt;code&gt;useEffect&lt;/code&gt; but never looked at what React does with it, you have all the background you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every useEffect call becomes an object on a fiber
&lt;/h2&gt;

&lt;p&gt;Every component you write gets turned into a fiber node, React's internal data structure for tracking everything about that component. It's the same structure behind &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;useState's state management&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When you call &lt;code&gt;useEffect&lt;/code&gt;, React doesn't run your function immediately. It &lt;strong&gt;creates an Effect object&lt;/strong&gt; and attaches it to the component's fiber. That object holds everything React needs to manage the effect's lifecycle.&lt;/p&gt;

&lt;p&gt;Here's what's inside it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tag&lt;/code&gt; marks whether the effect should actually run, based on whether its dependencies changed. React checks this flag before deciding to execute anything.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create()&lt;/code&gt; is your effect callback, the function you pass as the first argument to &lt;code&gt;useEffect&lt;/code&gt;. It holds the actual side-effect logic: fetching data, setting up a subscription, touching the DOM.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;destroy()&lt;/code&gt; starts out null. If your effect returns a cleanup function, React stores it here and calls it later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deps&lt;/code&gt; is your dependency array. React compares it against the previous render's array to decide whether the effect needs to run again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is React's to-do list for side effects: what to run, and when to clean it up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effects wait until React has updated the DOM
&lt;/h2&gt;

&lt;p&gt;React renders in two phases, render and commit, and &lt;strong&gt;effects only run once both are done&lt;/strong&gt;. This split is the key to understanding when &lt;code&gt;useEffect&lt;/code&gt; fires.&lt;/p&gt;

&lt;h3&gt;
  
  
  The render phase
&lt;/h3&gt;

&lt;p&gt;During render, React runs your component function, diffs the virtual DOM, and figures out what needs to change. This is also where it registers your &lt;code&gt;useEffect&lt;/code&gt; calls, it doesn't run them yet.&lt;/p&gt;

&lt;p&gt;This phase is also where reconciliation happens: React compares the new virtual DOM tree against the previous one to figure out the minimal set of real DOM changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The commit phase
&lt;/h3&gt;

&lt;p&gt;Once React knows what needs to change, it commits: it writes those changes to the real DOM. Only after that is done does React start running effects.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; runs after the commit phase, not during it. That ordering matters. If effects ran during commit, every effect would have to finish before the user saw any visual update. Running them after commit means the screen updates first and effects catch up in the background.&lt;/p&gt;

&lt;p&gt;This split also lets React batch effects together instead of running them one at a time as each component commits.&lt;/p&gt;

&lt;p&gt;That's the general case, not a guarantee. If the effect was triggered by a direct interaction like a click, React may run it before the browser paints, so the effect's result stays observable to the event system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependencies decide whether an effect runs again
&lt;/h2&gt;

&lt;p&gt;On every render, React compares an effect's dependencies with the previous render's, and &lt;strong&gt;only a changed value marks the effect to run&lt;/strong&gt;. Each &lt;code&gt;useEffect&lt;/code&gt; call in your component creates an entry in that component's hook list, a linked list attached to its fiber.&lt;/p&gt;

&lt;p&gt;Here's what happens on each render:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;React creates or updates the Effect object for that call.&lt;/li&gt;
&lt;li&gt;It retrieves the dependency array from the previous render.&lt;/li&gt;
&lt;li&gt;It compares the new dependencies against the old ones using &lt;code&gt;Object.is()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any dependency changed, or if there's no dependency array at all, React marks the effect to run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hasChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldDeps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;hasChanged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oldDeps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasChanged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Schedule this effect for execution&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Object.is()&lt;/code&gt; matters here because it handles edge cases regular equality misses. &lt;code&gt;Object.is(NaN, NaN)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;; &lt;code&gt;NaN === NaN&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;. That precision keeps dependency tracking correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Effects usually run after the browser paints
&lt;/h2&gt;

&lt;p&gt;Once React finishes committing DOM updates, it moves into the post-commit phase, where effects execute, and &lt;strong&gt;it lets the browser paint before it starts&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;React flushes all pending DOM updates, so every visual change is applied before any effect runs.&lt;/li&gt;
&lt;li&gt;It collects every effect marked for execution across all components in the current render cycle.&lt;/li&gt;
&lt;li&gt;It runs cleanup functions from the previous round of effects first, before running the new ones.&lt;/li&gt;
&lt;li&gt;It runs the new effect callbacks asynchronously, through React's own Scheduler rather than a microtask.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last step trips people up. &lt;code&gt;queueMicrotask()&lt;/code&gt; looks like the obvious tool for "soon, but not right now," but it isn't what React uses here. Microtasks drain before the browser gets a chance to paint, so effects scheduled that way would fire before the screen updated instead of after, undoing the whole point of deferring them. React posts the flush through &lt;code&gt;MessageChannel&lt;/code&gt; instead, the same mechanism its Scheduler package uses elsewhere, a macrotask-like hand-off that lets the browser paint first and gives effects the next turn after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup runs before the next effect and on unmount
&lt;/h2&gt;

&lt;p&gt;If your effect returns a function, React stores it as the cleanup for that effect and &lt;strong&gt;calls it before the next run of that effect, or when the component unmounts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what happens without one. This effect starts polling a chat room, and restarts whenever &lt;code&gt;roomId&lt;/code&gt; changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch rooms twice and three intervals are running, one per room you've visited, each still fetching and holding on to its old &lt;code&gt;roomId&lt;/code&gt;. Returning a cleanup fixes it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscriptions, timers, and event listeners all hold references that keep objects alive in memory. Without cleanup, those references pile up and the app leaks memory over time.&lt;/p&gt;

&lt;p&gt;React calls cleanup functions in two situations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before the next effect runs, if dependencies changed. This stops subscriptions or timers from overlapping.&lt;/li&gt;
&lt;li&gt;When the component unmounts, so no reference outlives the component. That includes when an error boundary catches a render error and unmounts the failing subtree, so a render error doesn't skip cleanup and leak memory anyway.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't have to track any of this timing yourself. React handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Children's effects run before their parent's
&lt;/h2&gt;

&lt;p&gt;Before reading on, guess what this logs when &lt;code&gt;Parent&lt;/code&gt; mounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'child'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'parent'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Child&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most people expect &lt;code&gt;parent&lt;/code&gt; first, since &lt;code&gt;Parent&lt;/code&gt; renders first. It prints &lt;code&gt;child&lt;/code&gt;, then &lt;code&gt;parent&lt;/code&gt;. React renders top-down, but &lt;strong&gt;it runs effects bottom-up&lt;/strong&gt;. React runs effects in a fixed, predictable order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Child effects run before parent effects, so a child's updates settle before the parent's do.&lt;/li&gt;
&lt;li&gt;Cleanup functions run before new effects, so side effects never overlap.&lt;/li&gt;
&lt;li&gt;Unmount cleanups always run, so nothing gets left behind when a component is removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ordering isn't arbitrary. If a parent's effects ran before its children's, the parent could end up processing data the children haven't finished updating. And if new effects ran before old ones cleaned up, you could end up with two WebSocket connections open at once instead of one, causing duplicate data or race conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks are matched by call order
&lt;/h2&gt;

&lt;p&gt;React keeps a linked list of hooks attached to each fiber. Every &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useState&lt;/code&gt;, or &lt;code&gt;useMemo&lt;/code&gt; call adds an entry to that list.&lt;/p&gt;

&lt;p&gt;This is the reason the Rules of Hooks exist. React matches hook state between renders by call order, the same mechanism behind &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;useState's hook registration&lt;/a&gt;. Change the order hooks are called in between renders, and that mapping breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hook&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Stores the effect object&lt;/span&gt;
    &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Pointer to the next hook&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React walks this list in order on every render, matching each hook call to its entry. Reorder the calls, for instance by putting a hook inside a conditional, and React can end up handing a &lt;code&gt;useEffect&lt;/code&gt; hook the state meant for a &lt;code&gt;useState&lt;/code&gt; hook. That's why hooks can't live inside loops, conditions, or nested functions: React needs to know exactly how many hooks will run, and in what order, to keep the mapping straight.&lt;/p&gt;

&lt;h2&gt;
  
  
  useLayoutEffect is the one that blocks paint
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; usually lets the screen update first, while &lt;code&gt;useLayoutEffect&lt;/code&gt; always holds it up. Running effects asynchronously buys a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rendering never waits on an effect to finish.&lt;/li&gt;
&lt;li&gt;The browser paints before effects run, not after.&lt;/li&gt;
&lt;li&gt;Effects from one render cycle run together instead of one at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useLayoutEffect&lt;/code&gt; is the exception: it runs synchronously, before paint. That's useful for DOM measurements or layout adjustments, but it can block the UI if the work takes too long.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook type&lt;/th&gt;
&lt;th&gt;Execution timing&lt;/th&gt;
&lt;th&gt;Blocking?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After paint (asynchronous)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useLayoutEffect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before paint (synchronous)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The async default matters most while a user is actively interacting, scrolling a list or typing into a field. If effects ran synchronously, they could stall the response to that interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four common misreadings of useEffect
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"&lt;code&gt;useEffect&lt;/code&gt; runs during render."&lt;/strong&gt; It doesn't. React only schedules it during render; the actual call happens after commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Dependencies cause re-renders."&lt;/strong&gt; They don't. Dependencies decide whether the effect re-runs, not whether the component re-renders. The component can re-render for unrelated reasons, like a state or prop change, while the effect stays untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Cleanup runs instantly."&lt;/strong&gt; It runs before the next effect, or on unmount, not the moment the effect finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Every dependency must be in the array, no exceptions."&lt;/strong&gt; The lint rule is a safe default, not a law. React's own guidance is that an effect should synchronize with something outside React, not fire in response to every value it happens to read. If you're fighting the array just to keep a function or value "current" without wanting to restart the effect, that's usually a sign the logic belongs in a ref, an event handler, or shouldn't be an effect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep effects small and their dependencies honest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep dependency arrays accurate and minimal. Every extra dependency gets compared on every render; every missing one risks a stale closure.&lt;/li&gt;
&lt;li&gt;Split unrelated effects into separate &lt;code&gt;useEffect&lt;/code&gt; calls. It's easier to debug, and React can skip each one independently.&lt;/li&gt;
&lt;li&gt;Reach for &lt;code&gt;useLayoutEffect&lt;/code&gt; only when you need a DOM measurement or a synchronous layout update. &lt;code&gt;useEffect&lt;/code&gt; is the right default otherwise.&lt;/li&gt;
&lt;li&gt;Don't use an effect to compute a value from props or state. Calculate it during render, with &lt;code&gt;useMemo&lt;/code&gt; if it's expensive. An effect would render once with the stale value, then again with the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my useEffect run twice in development?
&lt;/h3&gt;

&lt;p&gt;That's Strict Mode. In development, React intentionally mounts, unmounts, and remounts your component to check that your cleanup function actually works. In production, effects run once. Write effects so they behave correctly even when called more than once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use useEffect for data fetching?
&lt;/h3&gt;

&lt;p&gt;Yes, with some care around cleanup and error handling. A common pattern uses a flag to skip &lt;code&gt;setState&lt;/code&gt; calls after the component has unmounted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;fetchData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag makes sure &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;&lt;code&gt;setState&lt;/code&gt;&lt;/a&gt; never fires after teardown. Once the effect has dependencies, such as &lt;code&gt;[userId]&lt;/code&gt;, the same flag also stops a slow, older response from overwriting a newer one. React 18 removed the old "Can't perform a React state update on an unmounted component" warning, but ignoring stale results still matters. For anything more involved, a library like React Query handles the caching and race conditions for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between useEffect and useLayoutEffect?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; runs asynchronously, after the browser paints. &lt;code&gt;useLayoutEffect&lt;/code&gt; runs synchronously, before paint. Use &lt;code&gt;useLayoutEffect&lt;/code&gt; only when you need to measure the DOM or apply a layout change before the user sees anything, otherwise the extra synchronous work just delays the paint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do I need to include functions in my dependency array?
&lt;/h3&gt;

&lt;p&gt;Functions are objects in JavaScript, and a new one gets created on every render. If your effect uses a function from props or state and you leave it out of the dependency array, the effect keeps calling whatever version of that function existed when it first ran, a stale closure. The same issue shows up with &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;stale state values in useState&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I skip the dependency array?
&lt;/h3&gt;

&lt;p&gt;You can, but the effect will then run after every render, which usually isn't what you want and can cause a render loop. Include exactly the dependencies the effect reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I clean up subscriptions in useEffect?
&lt;/h3&gt;

&lt;p&gt;Return a function from the effect. React calls it before the next effect runs and again when the component unmounts, which is where you clear intervals, cancel in-flight requests, or remove event listeners. Anything that keeps a reference alive after your component is gone needs to be released here, following the same lifecycle React uses to manage &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;state cleanup&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; looks like a simple API, but underneath it's a fiber-based scheduling system deciding exactly when your side effects are allowed to run.&lt;/p&gt;

&lt;p&gt;Knowing how dependency comparison, scheduling, and cleanup fit together makes your effects more predictable, and makes the bugs easier to track down when they happen. Paired with how &lt;a href="https://blog.sachinthagunaratne.com/posts/how-usestate-works-under-the-hood" rel="noopener noreferrer"&gt;useState&lt;/a&gt; manages state internally, that's most of what you need to know about how React's hook system actually works.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.sachinthagunaratne.com/posts/how-useeffect-works-behind-the-scenes-in-react/" rel="noopener noreferrer"&gt;Notes by Sachintha&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
