<?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>Future: DevUnionX</title>
    <description>The latest articles on Future by DevUnionX (@devunionx).</description>
    <link>https://future.forem.com/devunionx</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg</url>
      <title>Future: DevUnionX</title>
      <link>https://future.forem.com/devunionx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://future.forem.com/feed/devunionx"/>
    <language>en</language>
    <item>
      <title>5 Things AI Can't Do, Even in React Testing Library</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Tue, 28 Apr 2026 02:02:29 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-react-testing-library-34o1</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-react-testing-library-34o1</guid>
      <description>&lt;h2&gt;
  
  
  I Spent Three Years Writing the Wrong React Tests. Here's What I Wish Someone Had Told Me.
&lt;/h2&gt;

&lt;p&gt;If you wonder my game:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FZ4J_ayp4GTnVs1XnvyIBAgkvjLK1jUcUu8MViVnx38_7n-JVf6mMYN3_ySqts4fSx26fb1aZVkeS9Ne7DbA9VA" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" rel="noopener noreferrer" class="c-link"&gt;
            Electricity Tycoon - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Build your electricity empire from a hand crank to industrial power plants.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The first React test I ever wrote checked that a component's state had a property called &lt;code&gt;count&lt;/code&gt; and that it equaled zero. I was using Enzyme. I was very proud. The test passed. Six weeks later we refactored that component to use a reducer and renamed &lt;code&gt;count&lt;/code&gt; to &lt;code&gt;value&lt;/code&gt;, and seventeen tests in the same file went red even though the application worked exactly the same as before. I spent a Friday afternoon updating tests that tested nothing useful.&lt;/p&gt;

&lt;p&gt;If you've been writing React tests for any length of time, you've probably had this experience. The tests pass. The tests fail. The tests don't seem to care whether your application actually works for an actual human being. You start to wonder if testing is just an elaborate ceremony we perform to feel professional.&lt;/p&gt;

&lt;p&gt;It isn't. But the tools we used to use made it feel that way, and the tools we use now — primarily React Testing Library — have a philosophy that makes everything click into place once you internalize it. This article is the long version of "internalize it." Get coffee. We're going to talk about queries, async, the &lt;code&gt;act()&lt;/code&gt; warning that has eaten more developer hours than any other single warning in frontend history, why your snapshots are lying to you, and what the actual state of React testing looks like in April 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sentence That Changed React Testing
&lt;/h2&gt;

&lt;p&gt;There is one sentence on the testing-library.com docs that you should tattoo on the inside of your eyelids:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The more your tests resemble the way your software is used, the more confidence they can give you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. That's the whole framework. Everything that follows in this article — every query priority, every async pattern, every reason to delete your snapshot tests — is a consequence of that one sentence.&lt;/p&gt;

&lt;p&gt;Kent C. Dodds wrote it in a tweet years ago, then it became the project tagline, then it became the README of every Testing Library repo, and now it's basically the unofficial constitution of the React testing world. The reason it matters is that it tells you what to optimize for. You're not optimizing for code coverage. You're not optimizing for "every function has a test." You're optimizing for &lt;strong&gt;confidence that your application works for the people who use it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most of the bad testing habits I had with Enzyme came from optimizing for the wrong thing. I was optimizing for "every component has a corresponding test file with the same name." I was optimizing for "every line of code is covered." I was optimizing for the dashboard at the top of our CI tool. None of those things told me whether the user could actually click the button and have something happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Enzyme Died (And Why That Was a Good Thing)
&lt;/h2&gt;

&lt;p&gt;Enzyme is dead. I should say this with some kindness because Enzyme was important and a lot of us learned testing on it, but it's been clinically dead since December 21, 2019, when version 3.11.0 shipped and then nothing happened for the next six and a half years. There is no official React 17 adapter, no React 18 adapter, no React 19 adapter. Community-maintained adapters exist — &lt;code&gt;@wojtekmaj/enzyme-adapter-react-17&lt;/code&gt; and &lt;code&gt;@cfaester/enzyme-adapter-react-18&lt;/code&gt; — but the maintainer of one of them literally writes "you probably shouldn't use it" in the README. That's the state of things.&lt;/p&gt;

&lt;p&gt;The reason Enzyme died is interesting. It wasn't a security issue. It wasn't that React broke it on purpose. It was that the React team moved toward concurrent rendering and hooks, and Enzyme's whole API was built around the idea that you could reach into a component and inspect its instance — its state, its props, its internal methods. With hooks, there's no instance to inspect. There's just a function that runs. Enzyme's mental model didn't have a place for that.&lt;/p&gt;

&lt;p&gt;But the deeper reason Enzyme died is that the way it encouraged you to write tests was structurally wrong. You'd render a component, then assert on its state. You'd call &lt;code&gt;wrapper.instance().handleClick()&lt;/code&gt; directly. You'd use &lt;code&gt;shallow()&lt;/code&gt; rendering, which renders only one component without its children, and then you'd assert that a child component received certain props. None of these things are things a user does. A user doesn't know your component has state. A user doesn't call your handler functions directly. A user doesn't know what's a parent and what's a child.&lt;/p&gt;

&lt;p&gt;Kent C. Dodds wrote a post called "Testing Implementation Details" that still gets quoted constantly. The line that landed for me was about renaming. Imagine your component has a state variable called &lt;code&gt;openIndex&lt;/code&gt;. You can rename it to &lt;code&gt;openIndexes&lt;/code&gt;, or to &lt;code&gt;tacosAreTasty&lt;/code&gt;, and the application works exactly the same. The user can't tell. The interface to the outside world hasn't changed. But every test that asserted on &lt;code&gt;openIndex&lt;/code&gt; just broke. That's a false negative. The test failed but nothing is broken.&lt;/p&gt;

&lt;p&gt;The opposite happens too. You can break the application — say, the click handler stops actually doing anything — and your test still passes because it was checking that &lt;code&gt;state.count&lt;/code&gt; got incremented, not that anything appeared on the screen. That's a false positive. The test passed but everything is broken.&lt;/p&gt;

&lt;p&gt;When most of your test failures fall into one of those two categories, you stop trusting your tests. And once you stop trusting your tests, you might as well not have them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Query Priority Hierarchy (And Why It Exists)
&lt;/h2&gt;

&lt;p&gt;React Testing Library gives you a bunch of ways to find elements in the DOM. They're not equivalent. They're ranked, and the ranking is opinionated, and the opinion comes from that one sentence about resembling how users use your software.&lt;/p&gt;

&lt;p&gt;Here's the order, with my unsolicited commentary on each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByRole&lt;/code&gt;&lt;/strong&gt; is the one you should reach for first. Almost always. If you're writing a test and you find yourself reaching for something else, ask whether you can use &lt;code&gt;getByRole&lt;/code&gt; instead. It works for buttons, links, headings, form inputs (textbox, checkbox, radio, combobox), regions, dialogs, lists — basically every meaningful piece of a web interface. The reason it's at the top is that ARIA roles are how assistive technology like screen readers understands your page. If your test can find a button by its accessible role and name, then a screen reader user can find that button too. The query is also surprisingly tolerant. &lt;code&gt;screen.getByRole('button', {name: /submit/i})&lt;/code&gt; finds a button whose accessible name contains "submit", case-insensitive. You don't need exact text. You don't need an ID. You're describing the button the way a person would describe it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByLabelText&lt;/code&gt;&lt;/strong&gt; is for form fields. When a sighted user fills out a form, they look at the label and then click the field next to it. When a screen reader user fills out a form, the label is announced when the field is focused. Either way, the label is the user's way in. So &lt;code&gt;screen.getByLabelText(/email/i)&lt;/code&gt; is how your test should find the email field, not by some &lt;code&gt;data-cy="email-input"&lt;/code&gt; attribute. The bonus is that if your test breaks because the label disappeared, that's actually a real bug — your form just became inaccessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByPlaceholderText&lt;/code&gt;&lt;/strong&gt; is a fallback for when a field has no label. The docs are clear about this: a placeholder is not a substitute for a label. But sometimes you're testing legacy code and there's just no label. Fine. Use it. Then file a ticket to add a label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByText&lt;/code&gt;&lt;/strong&gt; is for non-interactive content. Headings, paragraphs, status messages, error text. If the user can read it on the page, your test can find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByDisplayValue&lt;/code&gt;&lt;/strong&gt; finds form elements by their current value. Useful when you're testing that a form is pre-populated with edit data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getByAltText&lt;/code&gt;&lt;/strong&gt; for images. &lt;strong&gt;&lt;code&gt;getByTitle&lt;/code&gt;&lt;/strong&gt; is rarely useful and the docs warn that title attributes aren't consistently announced by screen readers anyway.&lt;/p&gt;

&lt;p&gt;And finally, at the bottom: &lt;strong&gt;&lt;code&gt;getByTestId&lt;/code&gt;&lt;/strong&gt;. This is the escape hatch. The docs literally say "the user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense." If your codebase has &lt;code&gt;data-testid&lt;/code&gt; sprinkled on every other element, that's a smell. It usually means somebody learned testing-library by Googling "how do I find an element in RTL" and the first Stack Overflow answer said &lt;code&gt;getByTestId&lt;/code&gt;. There's a reason it's last on the list.&lt;/p&gt;

&lt;p&gt;The mental shift takes a while. The first time I sat down to convert a test to use &lt;code&gt;getByRole&lt;/code&gt; instead of &lt;code&gt;container.querySelector('.submit-btn')&lt;/code&gt;, I had to actually look at my component and ask "wait, is this even a button? Or did I make a div with an onClick?" Half the time the answer was "I made a div with an onClick" and the test was forcing me to fix the accessibility of the component before I could even test it. That's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  getBy, queryBy, findBy — The Cheat Sheet Nobody Reads
&lt;/h2&gt;

&lt;p&gt;There are three flavors of every query, and developers confuse them constantly. Here's what they do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getBy*&lt;/code&gt;&lt;/strong&gt; throws an error if the element isn't found, returns it if it is. Use this for "I'm asserting this element exists." If you do &lt;code&gt;screen.getByRole('button', {name: /submit/i})&lt;/code&gt; and there's no submit button, your test fails with a useful error message that includes the entire DOM and a list of all the roles that &lt;em&gt;do&lt;/em&gt; exist. Helpful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;queryBy*&lt;/code&gt;&lt;/strong&gt; returns null if the element isn't found, returns it if it is. Use this for "I'm asserting this element does NOT exist." Because if you used &lt;code&gt;getByText('Error')&lt;/code&gt; to assert that no error was shown, the &lt;code&gt;getBy&lt;/code&gt; would throw before your assertion ever ran. So you do &lt;code&gt;expect(screen.queryByText('Error')).not.toBeInTheDocument()&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;findBy*&lt;/code&gt;&lt;/strong&gt; returns a Promise that resolves when the element appears, or rejects after a timeout (default 1000ms). Use this for "I'm asserting this element will appear, but not synchronously." Anything triggered by an async effect, a network request, a state update after a user event — &lt;code&gt;findBy*&lt;/code&gt;. The docs put it plainly: &lt;code&gt;findBy*&lt;/code&gt; is literally &lt;code&gt;getBy*&lt;/code&gt; plus &lt;code&gt;waitFor&lt;/code&gt;. Just shorter.&lt;/p&gt;

&lt;p&gt;The number one mistake I see in code review is people using &lt;code&gt;getByText&lt;/code&gt; to check for something that hasn't appeared yet, then wondering why the test fails. The element isn't there yet. Wait for it. Use &lt;code&gt;findByText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The number two mistake is using &lt;code&gt;getByText&lt;/code&gt; to assert something is gone. Use &lt;code&gt;queryByText&lt;/code&gt;. The error you get from &lt;code&gt;getByText&lt;/code&gt; failing is "Unable to find an element with text," which is technically true but not what your test is trying to say.&lt;/p&gt;

&lt;p&gt;There's also &lt;code&gt;getAllBy*&lt;/code&gt;, &lt;code&gt;queryAllBy*&lt;/code&gt;, and &lt;code&gt;findAllBy*&lt;/code&gt; for when you expect multiple matches. The behavior is what you'd guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  fireEvent vs user-event: The Day I Learned to Add &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For the first year I used React Testing Library, I used &lt;code&gt;fireEvent&lt;/code&gt; for everything. &lt;code&gt;fireEvent.click(button)&lt;/code&gt;. &lt;code&gt;fireEvent.change(input, {target: {value: 'hello'}})&lt;/code&gt;. It worked. Tests passed. Life was good.&lt;/p&gt;

&lt;p&gt;Then I read the user-event docs and realized I'd been simulating the wrong thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fireEvent&lt;/code&gt; dispatches a single DOM event. Period. &lt;code&gt;fireEvent.click(button)&lt;/code&gt; fires a click event. That's it. No focus event, no mousedown, no mouseup. Just click. &lt;code&gt;fireEvent.change(input, {target: {value: 'hello'}})&lt;/code&gt; fires a single change event with the value already set. The user never actually typed anything. The input was never focused. No keystrokes happened. From the DOM's perspective, the value just teleported in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user-event&lt;/code&gt; simulates what actually happens when a real human interacts with the page. &lt;code&gt;user.click(button)&lt;/code&gt; triggers a &lt;code&gt;mousedown&lt;/code&gt;, then a &lt;code&gt;mouseup&lt;/code&gt;, then a &lt;code&gt;click&lt;/code&gt;, and along the way it focuses the button if it's focusable. &lt;code&gt;user.type(input, 'hello')&lt;/code&gt; focuses the input, then for each character fires a &lt;code&gt;keydown&lt;/code&gt;, a &lt;code&gt;keypress&lt;/code&gt;, an &lt;code&gt;input&lt;/code&gt; event, and a &lt;code&gt;keyup&lt;/code&gt;. It manipulates the cursor position. It does what a real keyboard does.&lt;/p&gt;

&lt;p&gt;This matters more than you'd think. There are bugs that only appear when events fire in a specific order. There are form libraries (looking at you, react-hook-form) that depend on focus and blur events to run validation. There are accessibility behaviors that hinge on whether an element actually got focused. If you use &lt;code&gt;fireEvent&lt;/code&gt;, none of this gets exercised. Your test runs in a fantasy world where state just changes.&lt;/p&gt;

&lt;p&gt;In version 14 of &lt;code&gt;user-event&lt;/code&gt; (released March 2022, currently at 14.6.1 as of January 2025), every interaction became asynchronous. This was a big migration pain at the time and is still tripping people up in 2026 because half the tutorials online are from before the change. The new pattern looks like this:&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;import&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submits the form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyForm&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;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/submit/i&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/thanks/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Notice three things: &lt;code&gt;userEvent.setup()&lt;/code&gt; is called once at the top, every interaction is &lt;code&gt;await&lt;/code&gt;ed, and we use &lt;code&gt;findByText&lt;/code&gt; for content that appears after async work.&lt;/p&gt;

&lt;p&gt;The number one symptom of forgetting an &lt;code&gt;await&lt;/code&gt; somewhere is the warning we're about to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The act() Warning: A Eulogy In Three Acts
&lt;/h2&gt;

&lt;p&gt;If you've written React tests and never seen the &lt;code&gt;act()&lt;/code&gt; warning, you have not actually written React tests. Here it is, in its full glory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: An update to ComponentName inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() =&amp;gt; {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have seen this warning in production codebases. I have seen it in tutorials. I have seen it in pull request descriptions where the author says "ignore the warnings, the tests pass." I have personally spent multiple full evenings hunting down a single act warning in a single test file. It is the most universally hated message in the React ecosystem and it is also, somewhat unfairly, almost always your fault.&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;act()&lt;/code&gt; actually is. React batches state updates and side effects in production. When you're testing, you want to make assertions on the DOM after all those batched updates have flushed. &lt;code&gt;act()&lt;/code&gt; is the mechanism that says "do all the React work, including effects, before this function returns." If you cause a state update without it being inside an &lt;code&gt;act()&lt;/code&gt; boundary, React doesn't know whether you're going to do more updates next, so it warns you that the test environment isn't behaving like the browser.&lt;/p&gt;

&lt;p&gt;The good news is that React Testing Library wraps almost everything in &lt;code&gt;act()&lt;/code&gt; for you. &lt;code&gt;render()&lt;/code&gt; is wrapped. &lt;code&gt;fireEvent&lt;/code&gt; is wrapped. &lt;code&gt;waitFor&lt;/code&gt; is wrapped. &lt;code&gt;findBy*&lt;/code&gt; is wrapped. &lt;code&gt;user-event&lt;/code&gt; v14 is wrapped. You almost never need to call &lt;code&gt;act()&lt;/code&gt; yourself.&lt;/p&gt;

&lt;p&gt;So why do you keep seeing the warning?&lt;/p&gt;

&lt;p&gt;Because something is updating state after your test thinks it's done. The most common cause is a &lt;code&gt;useEffect&lt;/code&gt; that runs after initial render and triggers a state update. Your test renders the component, makes its assertions, and exits — and then the effect fires, and React tries to update state, and there's no test left to be inside an &lt;code&gt;act()&lt;/code&gt; boundary.&lt;/p&gt;

&lt;p&gt;The fix is almost always to wait for the state to settle before your test ends. If you have a component that fetches data on mount, your test should look like:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows the user list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/jane doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Notice the &lt;code&gt;await&lt;/code&gt; on &lt;code&gt;findByText&lt;/code&gt;. That's the magic. The test now waits until the data has loaded and the component has settled before it finishes. No more act warning.&lt;/p&gt;

&lt;p&gt;If your effect updates state but doesn't render anything new (say, it sets some internal flag), use &lt;code&gt;waitFor&lt;/code&gt;:&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;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockSomething&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using fake timers, that's a whole separate can of worms. &lt;code&gt;jest.useFakeTimers()&lt;/code&gt; runs code outside React's normal callstack, so the auto-&lt;code&gt;act&lt;/code&gt; doesn't catch it. You need to manually wrap the timer advancement:&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;await&lt;/span&gt; &lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAllTimers&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;If you're using a form library like react-hook-form with async validation, you'll see the warning when validation runs after submit. The fix is to await something that depends on the validation completing — like an error message appearing, or a submit handler being called.&lt;/p&gt;

&lt;p&gt;The thing not to do is wrap things that are already wrapped. I see this all the time:&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="c1"&gt;// Don't do this — render is already wrapped in act&lt;/span&gt;
&lt;span class="nf"&gt;act&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyComponent&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;You're double-wrapping. It doesn't break anything but it produces a different warning telling you about overlapping &lt;code&gt;act()&lt;/code&gt; calls. Stop.&lt;/p&gt;

&lt;p&gt;One more thing. In React 19 (which RTL 16.1.0 added support for, on December 5, 2024 — the same day React 19 went GA), &lt;code&gt;act&lt;/code&gt; is officially expected to be async. The synchronous version is deprecated. You should now import &lt;code&gt;act&lt;/code&gt; from &lt;code&gt;@testing-library/react&lt;/code&gt;, not from &lt;code&gt;react&lt;/code&gt;. RTL has been re-exporting it since version 15.0.6 (May 2024). If you have old test files that import &lt;code&gt;act&lt;/code&gt; from React, fix them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Testing: findBy, waitFor, and the Order of Operations
&lt;/h2&gt;

&lt;p&gt;Async testing in RTL has three primitives. You only really need to know two of them well, and the third is for edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;findBy*&lt;/code&gt;&lt;/strong&gt; is for waiting for an element to appear. Use this 80% of the time you need to wait for something. The default timeout is 1000ms, which is usually fine. If you need longer, you can override it: &lt;code&gt;screen.findByText(/loaded/i, {}, {timeout: 5000})&lt;/code&gt;. Avoid bumping the timeout in normal cases — it usually means your component is doing something genuinely slow that you should fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitFor&lt;/code&gt;&lt;/strong&gt; is for waiting for an arbitrary assertion to pass. Use this when what you're waiting for isn't an element appearing — like waiting for a mock function to have been called, or waiting for a fetch to have happened.&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="c1"&gt;// ✅ Good&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockOnSubmit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a@b.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Bad — putting side effects inside waitFor&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mockOnSubmit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&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 reason the second one is bad is that &lt;code&gt;waitFor&lt;/code&gt; runs its callback repeatedly until either the assertion passes or the timeout fires. If you have a click event inside there, you'll fire that click event many times. That's not what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;waitForElementToBeRemoved&lt;/code&gt;&lt;/strong&gt; is for waiting for something to disappear. Loading spinners, mostly. The pattern is:&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;await&lt;/span&gt; &lt;span class="nf"&gt;waitForElementToBeRemoved&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/loading/i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice we use &lt;code&gt;queryByText&lt;/code&gt; here, not &lt;code&gt;getByText&lt;/code&gt;. That's because at the moment the element is finally removed, &lt;code&gt;getByText&lt;/code&gt; would throw (the element isn't there anymore), which would defeat the whole purpose.&lt;/p&gt;

&lt;p&gt;The most common async testing mistake I've seen recently is using &lt;code&gt;waitFor&lt;/code&gt; to look for an element when &lt;code&gt;findBy&lt;/code&gt; would do:&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="c1"&gt;// ❌ Verbose&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Direct&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are functionally equivalent. &lt;code&gt;findBy*&lt;/code&gt; uses &lt;code&gt;waitFor&lt;/code&gt; internally. The second version is shorter, the error messages are better, and it makes intent clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mock Service Worker, or, Stop Mocking fetch
&lt;/h2&gt;

&lt;p&gt;If you're testing components that fetch data, you have a few options. Most of them are bad.&lt;/p&gt;

&lt;p&gt;You can mock &lt;code&gt;window.fetch&lt;/code&gt; with &lt;code&gt;jest.fn()&lt;/code&gt;. This works but it's brittle — every test has to set up the mock, you couple your tests to the exact shape of the request, and as soon as you switch from &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;axios&lt;/code&gt; to &lt;code&gt;ky&lt;/code&gt; to whatever's trendy this week, all your tests break.&lt;/p&gt;

&lt;p&gt;You can mock the network library directly. Same problem.&lt;/p&gt;

&lt;p&gt;You can use Mock Service Worker (MSW). This is what the testing-library docs themselves recommend. MSW intercepts requests at the network level — it doesn't care whether you used &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;axios&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, or carrier pigeons. You declare what your endpoints return, MSW handles the rest, and your component code is completely unchanged.&lt;/p&gt;

&lt;p&gt;The MSW v2 API looks like this:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jane Doe&lt;/span&gt;&lt;span class="dl"&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;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows the user list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/jane doe/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;If you have a test where you want to simulate an error, you override the handler for that one test:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error on server failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oops&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserList&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/something went wrong/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;This pattern has changed how I write integration tests. I now write tests that render entire features with actual components and actual routing, and the only thing mocked is the network. The tests look almost like end-to-end tests but run in milliseconds because there's no browser.&lt;/p&gt;

&lt;p&gt;Note that MSW v2 changed the API — if you're looking at older tutorials with &lt;code&gt;rest.get&lt;/code&gt; and &lt;code&gt;res(ctx.json(...))&lt;/code&gt;, that's the v1 syntax and it no longer works. The new shape is &lt;code&gt;http.get&lt;/code&gt; and &lt;code&gt;HttpResponse.json(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing React Query (Or TanStack Query, As The Cool Kids Now Call It)
&lt;/h2&gt;

&lt;p&gt;If your app uses React Query, you'll discover that your tests time out for no obvious reason. Then you'll discover that React Query retries failed queries three times by default with exponential backoff. That's great in production. In tests, it means a single failed query takes upwards of seven seconds before your test even sees the error.&lt;/p&gt;

&lt;p&gt;The fix is documented in TkDodo's blog (he's a React Query maintainer) and it's the canonical pattern:&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;function&lt;/span&gt; &lt;span class="nf"&gt;createWrapper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;children&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&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="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/QueryClientProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows error on failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/data&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyComponent&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;span class="na"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWrapper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/error/i&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeInTheDocument&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;Two things to notice. First, you create a fresh &lt;code&gt;QueryClient&lt;/code&gt; per test. Don't share one. Tests will pollute each other through the cache. Second, &lt;code&gt;retry: false&lt;/code&gt;. Always. In tests, you want immediate failure.&lt;/p&gt;

&lt;p&gt;If you forget to wrap your component in a &lt;code&gt;QueryClientProvider&lt;/code&gt; at all, you'll get the very recognizable &lt;code&gt;No QueryClient set, use QueryClientProvider to set one&lt;/code&gt; error. That one I've seen in roughly seventy percent of "help me debug my test" Slack threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Hooks With renderHook
&lt;/h2&gt;

&lt;p&gt;You can test custom hooks directly with &lt;code&gt;renderHook&lt;/code&gt;. This used to live in a separate package called &lt;code&gt;@testing-library/react-hooks&lt;/code&gt;, which was deprecated in 2022 when &lt;code&gt;renderHook&lt;/code&gt; got moved into the main &lt;code&gt;@testing-library/react&lt;/code&gt; package starting in version 13.1.&lt;/p&gt;

&lt;p&gt;The basic pattern:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderHook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;act&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCounter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./useCounter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increments the counter&lt;/span&gt;&lt;span class="dl"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&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;useCounter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="nf"&gt;act&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;If your hook needs context, use the &lt;code&gt;wrapper&lt;/code&gt; option:&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderHook&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;useMyHook&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyProvider&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MyProvider&lt;/span&gt;&lt;span class="err"&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;A small honesty note: the new &lt;code&gt;renderHook&lt;/code&gt; in &lt;code&gt;@testing-library/react&lt;/code&gt; is less full-featured than the old &lt;code&gt;@testing-library/react-hooks&lt;/code&gt; package. It doesn't have first-class SSR support. There are some &lt;code&gt;waitForNextUpdate&lt;/code&gt; patterns from the old package that have to be rewritten with &lt;code&gt;waitFor&lt;/code&gt;. If you migrated and felt like something was missing, you weren't imagining it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing: MemoryRouter Is Your Friend
&lt;/h2&gt;

&lt;p&gt;Testing components that use React Router requires giving them a router. You don't want the real &lt;code&gt;BrowserRouter&lt;/code&gt; because that touches &lt;code&gt;window.history&lt;/code&gt;, which is shared across tests and causes pollution. You want &lt;code&gt;MemoryRouter&lt;/code&gt;, which keeps its history in memory:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MemoryRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MemoryRouter&lt;/span&gt; &lt;span class="nx"&gt;initialEntries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/42&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserPage&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Routes&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MemoryRouter&lt;/span&gt;&lt;span class="err"&gt;&amp;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 &lt;code&gt;initialEntries&lt;/code&gt; prop sets where the router thinks you are. It only takes paths starting with &lt;code&gt;/&lt;/code&gt;, not full URLs (this catches people).&lt;/p&gt;

&lt;p&gt;If you're testing a component that triggers navigation, you can either render multiple routes and assert that you ended up on the right page (which is how a user would experience it), or you can use the &lt;code&gt;useLocation&lt;/code&gt; hook in a test helper component and assert on the location object. The first option is more behavior-focused and is what I usually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vitest Is Eating Jest's Lunch
&lt;/h2&gt;

&lt;p&gt;A note on the surrounding ecosystem because it's been shifting fast. As of 2026, Vitest has effectively passed Jest in adoption for new projects. The State of JS 2024 survey put Vitest at the top for retention and positivity rankings, and the weekly download trend lines crossed sometime in late 2025. Angular 21 (released late 2025) made Vitest the default test runner. Nuxt, SvelteKit, and Astro all recommend it. Jest is not dead — it's still huge, especially in legacy codebases — but the momentum is gone.&lt;/p&gt;

&lt;p&gt;For React Testing Library, this matters approximately zero. RTL is test-runner-agnostic. The same &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;screen&lt;/code&gt;, and &lt;code&gt;userEvent&lt;/code&gt; work identically whether you're using Jest or Vitest. The difference is in setup and the matchers.&lt;/p&gt;

&lt;p&gt;For Jest, you import jest-dom matchers like this:&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;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Vitest, you need a slightly different import to register the matchers with Vitest's &lt;code&gt;expect&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/jest-dom/vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's about it. The actual test code is identical.&lt;/p&gt;

&lt;p&gt;A word of caution: Vitest is not always faster than Jest. There are real-world reports of teams migrating from Jest to Vitest and getting slower test suites, especially with large React codebases. The default Vitest configuration uses Vite's transform pipeline, which can be slower for some setups than Jest's babel-based pipeline. The trick most teams that get fast Vitest results use is &lt;code&gt;happy-dom&lt;/code&gt; instead of &lt;code&gt;jsdom&lt;/code&gt; for the DOM environment. That alone often cuts test times in half. Your mileage will absolutely vary, so benchmark before you commit to a migration.&lt;/p&gt;

&lt;p&gt;If you want one less thing to think about, keep using Jest. If you're starting a new project and using Vite for your build, Vitest is the natural choice. Either is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anti-Patterns That Are Killing Your Tests
&lt;/h2&gt;

&lt;p&gt;Let me list, in roughly the order I see them in code review, the things you should stop doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop using &lt;code&gt;container.querySelector&lt;/code&gt;.&lt;/strong&gt; I know it's tempting. You know CSS selectors. You wrote one in twelve seconds. But every time you do &lt;code&gt;container.querySelector('.btn-primary')&lt;/code&gt;, you're testing a class name that has nothing to do with what the user sees. Change the class name during a styling refactor, the test breaks. It also gives the worst error messages of any query type. Use &lt;code&gt;getByRole&lt;/code&gt; or one of its siblings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop snapshot testing entire components.&lt;/strong&gt; I had to learn this one the hard way. We had a snapshot test for a dashboard component that was 640 lines long. Every time anyone changed anything in that dashboard, the snapshot would update, and the reviewer would scan the diff for ten seconds, see that all the changes "looked fine," and approve the snapshot update. We never caught a single bug with that snapshot. We just had a 640-line file in our repo that everyone learned to ignore. The Justin Searls quote that Kent C. Dodds quotes is the eulogy: "Most developers, upon seeing a snapshot test fail, will sooner just nuke the snapshot and record a fresh passing one instead of agonizing over what broke it." That's exactly what we did.&lt;/p&gt;

&lt;p&gt;Snapshots have a place. Tiny, focused snapshots of pure functions or deeply structured data are fine. Whole-component snapshots are almost always a smell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop testing component state.&lt;/strong&gt; If you find yourself writing &lt;code&gt;expect(wrapper.instance().state.count).toBe(1)&lt;/code&gt; you're back in Enzyme-land mentally. Test what's on the screen. If clicking the button increments a counter, assert that the displayed text changed from "0" to "1". The user can't see your state. Your test shouldn't either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop testing implementation methods directly.&lt;/strong&gt; No &lt;code&gt;expect(component.handleSubmit).toHaveBeenCalled()&lt;/code&gt;. Test that submitting the form calls the &lt;code&gt;onSubmit&lt;/code&gt; prop, or that an API call happens, or that the success message appears. Test the visible outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop over-mocking.&lt;/strong&gt; I saw a test recently that mocked four child components, the routing library, the date library, and a translation hook, all to "isolate" the component under test. By the time all the mocks were set up, the test was longer than the component, the component being tested wasn't really being tested anymore (since most of its actual collaborators had been replaced with stubs), and any refactor that moved logic between components broke the test. The Tahamjp dev.to article from 2025 has my favorite line on this: "Mocks are like duct tape — handy until you start covering the whole car with it."&lt;/p&gt;

&lt;p&gt;The right answer most of the time is: don't mock. Render the whole feature with MSW handling network. Let the components actually collaborate. Test what the user sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop wrapping things in &lt;code&gt;act()&lt;/code&gt; that are already wrapped.&lt;/strong&gt; I covered this above but it's worth repeating. &lt;code&gt;render&lt;/code&gt;, &lt;code&gt;fireEvent&lt;/code&gt;, &lt;code&gt;waitFor&lt;/code&gt;, &lt;code&gt;findBy*&lt;/code&gt;, and v14 &lt;code&gt;userEvent&lt;/code&gt; are all wrapped internally. Don't double-wrap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop using &lt;code&gt;fireEvent&lt;/code&gt; when &lt;code&gt;user-event&lt;/code&gt; would do.&lt;/strong&gt; Just use &lt;code&gt;user-event&lt;/code&gt;. The async pattern feels weird at first; you'll get used to it in a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop ignoring ESLint plugins.&lt;/strong&gt; There are two: &lt;code&gt;eslint-plugin-testing-library&lt;/code&gt; and &lt;code&gt;eslint-plugin-jest-dom&lt;/code&gt;. They catch most of the mistakes in this section automatically. Install them, set them to error, and let the linter teach your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Still Hard in 2026
&lt;/h2&gt;

&lt;p&gt;Honesty time. There are things React Testing Library doesn't do well, and pretending otherwise would be unfair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server Components.&lt;/strong&gt; There is no canonical way to unit-test an async React Server Component. RTL doesn't support them, the React team hasn't published a recommended pattern, and the Next.js documentation explicitly tells you to use end-to-end tests with Playwright instead. If your app is heavily server-rendered, expect a gap in your testing story. This is the single biggest unsolved problem in React testing right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portals.&lt;/strong&gt; Components that render into a portal (modals, tooltips, popovers built on Radix or Headless UI) sometimes mount outside your test container, and &lt;code&gt;screen.getByRole&lt;/code&gt; doesn't find them at first glance. The fix is to use &lt;code&gt;screen&lt;/code&gt; queries (which look at &lt;code&gt;document.body&lt;/code&gt;, not just the rendered container), or to pass &lt;code&gt;baseElement: document.body&lt;/code&gt; to your render. Once you know the trick it's fine, but the first time it happens you'll spend twenty minutes confused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animations and motion libraries.&lt;/strong&gt; framer-motion in particular has a reputation for being slow in jsdom and producing weird test failures. There are open issues going back years. The workarounds usually involve mocking the motion components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Suspense regression in React 19.&lt;/strong&gt; There's an open issue (#1375) where suspended components in tests sometimes keep rendering their fallbacks instead of their children. Affected at one point about 300 tests in the Vercel monorepo. Worth knowing about if you use Suspense heavily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The npm dependency hell of late 2024.&lt;/strong&gt; When React 19 went GA, RTL 16.0.x had a peer dependency on &lt;code&gt;@testing-library/dom@^10&lt;/code&gt;, but a lot of toolchains (especially Create React App, may it rest in peace) were locked to &lt;a href="mailto:dom@8"&gt;dom@8&lt;/a&gt;. The result was the dreaded ERESOLVE errors and a lot of frantic Stack Overflow questions. RTL 16.1.0 on December 5, 2024 fixed this the same day React 19 shipped, but a lot of people are still on 16.0.1 and don't know why their installs are angry.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Realistic Test File, From Top To Bottom
&lt;/h2&gt;

&lt;p&gt;Here's what I think a good RTL test file looks like in 2026. This is for a hypothetical login form.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@testing-library/user-event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;msw/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./LoginForm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&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="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LoginForm&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logs in with valid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitFor&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shows an error with invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveTextContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/invalid credentials/i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disables the submit button while submitting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LoginForm&lt;/span&gt; &lt;span class="nx"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/email/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabelText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/password/i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;correct&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/log in/i&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDisabled&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;Notice what's not there. No mocks of child components. No assertions on state. No &lt;code&gt;data-testid&lt;/code&gt; attributes. No snapshots. No checking that handlers were called with specific internal arguments. Every assertion is something a real user could verify by looking at or interacting with the page. The login flow is tested end to end with real network mocking.&lt;/p&gt;

&lt;p&gt;This is the shape of a healthy RTL test file. If your test files don't look like this, that's something to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing Argument
&lt;/h2&gt;

&lt;p&gt;When I started writing React tests with Enzyme, I thought testing was about coverage. Cover every line, cover every branch, sleep at night. When I switched to React Testing Library, I thought testing was about queries — learn the priority list, find the elements the right way, sleep at night.&lt;/p&gt;

&lt;p&gt;After a few years, I think testing is mostly about having a good answer to one question: &lt;strong&gt;if my application is broken, will my test suite tell me?&lt;/strong&gt; Not "will some test fail." Not "will coverage drop." Will my tests, the actual ones I wrote with intent, tell me that the user's experience is broken. If yes, the tests are doing their job. If no, they're decoration.&lt;/p&gt;

&lt;p&gt;React Testing Library doesn't make this question easier to answer by accident. It makes it easier on purpose, by forcing you to write tests in the language of the user instead of the language of the implementation. That's the whole thing. That's the only reason any of this matters. Every query priority, every async pattern, every anti-pattern in this article — they all come back to that.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;getByRole&lt;/code&gt;. Await your &lt;code&gt;user.click&lt;/code&gt;. Wait for things with &lt;code&gt;findBy*&lt;/code&gt;. Mock the network with MSW. Don't snapshot anything you wouldn't want to read. Don't reach into state. Don't test children you've already mocked. Trust your tests to fail loudly when something real is broken, and to stay quiet when you're just renaming a variable.&lt;/p&gt;

&lt;p&gt;If you do that, you'll spend less of your life arguing with the act warning, and more of your life shipping. Which, last time I checked, is what we're all here for.&lt;/p&gt;

&lt;p&gt;Now stop reading and go delete some snapshots.&lt;/p&gt;

</description>
      <category>react</category>
      <category>testing</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>!!!Hello</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 25 Apr 2026 17:05:24 +0000</pubDate>
      <link>https://future.forem.com/devunionx/hello-h4c</link>
      <guid>https://future.forem.com/devunionx/hello-h4c</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-story__hidden-navigation-link"&gt;5 Things AI Can't Do, Even in npm / yarn / pnpm&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3550601" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" id="article-link-3550601"&gt;
          5 Things AI Can't Do, Even in npm / yarn / pnpm
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/npm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;npm&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            21 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in npm / yarn / pnpm</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 25 Apr 2026 17:04:58 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-npm-yarn-pnpm-4ef5</guid>
      <description>&lt;h2&gt;
  
  
  I Stopped Caring About Package Managers. Then I Cared a Lot. Here's the Whole Story.
&lt;/h2&gt;

&lt;p&gt;This is my game by the way if you want to support..&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplay-lh.googleusercontent.com%2FZ4J_ayp4GTnVs1XnvyIBAgkvjLK1jUcUu8MViVnx38_7n-JVf6mMYN3_ySqts4fSx26fb1aZVkeS9Ne7DbA9VA" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://play.google.com/store/apps/details?id=com.electricitytycoon.app" rel="noopener noreferrer" class="c-link"&gt;
            Electricity Tycoon - Apps on Google Play
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Build your electricity empire from a hand crank to industrial power plants.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.gstatic.com%2Fandroid%2Fmarket_images%2Fweb%2Ffavicon_v3.ico"&gt;
          play.google.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;&lt;em&gt;Thank you&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few months ago I joined a project where the &lt;code&gt;node_modules&lt;/code&gt; folder was 2.4 gigabytes. Two point four. On a fresh clone. The &lt;code&gt;pnpm install&lt;/code&gt; — yes, they were already on pnpm — took fourteen seconds, but the &lt;strong&gt;first&lt;/strong&gt; install on a new laptop took closer to ninety because of the cold cache. I sat there watching the spinner and thinking about how, ten years ago, I was running &lt;code&gt;npm install&lt;/code&gt; on a Macbook Air with a spinning rust drive and an ExpressVPN connection from a coffee shop in Berlin, and it took roughly the same amount of time, except for a project that had maybe sixty dependencies instead of nine hundred.&lt;/p&gt;

&lt;p&gt;We've made progress. We've also made a mess. And the three tools we use to manage that mess — npm, yarn, and pnpm — are not interchangeable, even though every junior dev I've onboarded in the last three years thinks they are.&lt;/p&gt;

&lt;p&gt;This is the article I wish I'd had when I switched between all three for the first time. It's long. Get coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Where I Pretend You Need a History Lesson
&lt;/h2&gt;

&lt;p&gt;You probably don't, but the history actually matters because it explains why these tools behave the way they do. Decisions made in 2010 are still ruining your Tuesday in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm&lt;/strong&gt; showed up in 2010, written by Isaac Schlueter. It was bundled with Node.js starting in 2011 and that's basically why it won — it was the default. There was no real competition. If you wrote JavaScript on the server, you used npm. The registry, npmjs.com, also became the de facto package registry, which is why even today when you install something with yarn or pnpm, you're still pulling from npm's servers. The registry and the CLI are separate things, even though most people conflate them.&lt;/p&gt;

&lt;p&gt;For about five years, npm coasted on being the only option. And it had problems. Slow installs. Non-deterministic dependency resolution — meaning if you and I both ran &lt;code&gt;npm install&lt;/code&gt; on the same &lt;code&gt;package.json&lt;/code&gt;, we could end up with subtly different &lt;code&gt;node_modules&lt;/code&gt; trees, which led to the infamous "works on my machine" bug a thousand times over. No real lockfile until npm 5 in 2017. The registry itself had outages. And then, in March 2016, &lt;strong&gt;left-pad&lt;/strong&gt; happened.&lt;/p&gt;

&lt;p&gt;If you don't know the left-pad incident: a developer named Azer Koçulu got into a naming dispute with Kik (the messaging company) over an npm package called &lt;code&gt;kik&lt;/code&gt;. npm sided with Kik and transferred the package. Azer, in protest, unpublished all 250 of his packages. One of them was &lt;code&gt;left-pad&lt;/code&gt;, an eleven-line function that pads strings on the left with zeros. It had millions of downloads a week. Within minutes, half the JavaScript ecosystem broke. React broke. Babel broke. Build pipelines around the world fell over. It was genuinely funny and also genuinely terrifying.&lt;/p&gt;

&lt;p&gt;The aftermath of left-pad was that npm changed its policies on unpublishing, but the deeper damage was a loss of trust. People started looking around. And in October 2016, &lt;strong&gt;Yarn&lt;/strong&gt; appeared, made by Facebook (with help from Google, Exponent, and Tilde). It was a direct response to npm's problems: deterministic installs, a real lockfile, parallel network requests, offline caching. The first time I ran &lt;code&gt;yarn install&lt;/code&gt; on a project that took two minutes with npm and watched it finish in twenty seconds, I genuinely thought something had broken.&lt;/p&gt;

&lt;p&gt;Yarn embarrassed npm into getting better. npm 5 added &lt;code&gt;package-lock.json&lt;/code&gt; (yes, they had to copy yarn's idea), npm 6 added &lt;code&gt;npm audit&lt;/code&gt;, npm 7 added workspaces, and so on. But while npm was catching up, Yarn was about to fork itself in half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yarn 2&lt;/strong&gt; — codenamed Berry — came out in early 2020, and it was a ground-up rewrite. The flagship feature was Plug'n'Play (PnP), which we'll get to in a minute. It was so different from Yarn 1 that the community basically split. Some teams went with Berry. Most stayed on Yarn 1. The Yarn 1 codebase got moved to "Yarn Classic" status — still working, but not really developed anymore. As of right now in 2026, Yarn 1 is still installed on more machines than Yarn 4. Let that sink in.&lt;/p&gt;

&lt;p&gt;Meanwhile, in 2017, Zoltan Kochan released &lt;strong&gt;pnpm&lt;/strong&gt;. The "p" stands for "performant," but the real innovation wasn't speed — it was the storage model. pnpm built a content-addressable store on your disk. One copy of &lt;code&gt;lodash@4.17.21&lt;/code&gt; on your whole machine, hard-linked into every project that needs it. If you've ever had ten projects and ten copies of &lt;code&gt;node_modules&lt;/code&gt;, each weighing in at hundreds of megabytes, this is appealing.&lt;/p&gt;

&lt;p&gt;For years pnpm was the weird underdog. The "actually I use Arch btw" of package managers. Then around 2022, things started shifting. Vue switched to pnpm. Vite uses pnpm. Astro uses pnpm. SvelteKit's &lt;code&gt;create-svelte&lt;/code&gt; defaults to it for monorepo setups. Microsoft uses it internally. The state-of-JS surveys started showing pnpm climbing every year. And by 2024, pnpm wasn't a niche choice anymore — it was the default for new TypeScript-heavy monorepos.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;Bun&lt;/strong&gt; showed up in late 2022 doing absolutely everything (runtime, bundler, test runner, package manager) at speeds that, when I first benchmarked them, made me check my command twice. We'll come back to Bun.&lt;/p&gt;

&lt;h2&gt;
  
  
  How They Actually Work (And Why It Matters For Your Bugs)
&lt;/h2&gt;

&lt;p&gt;Here's the section that most "npm vs yarn vs pnpm" articles skip or hand-wave through, because it's the boring part. It's also the only part that matters when something breaks at 11pm on a Friday before a deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  npm and Yarn Classic: The Flat-ish, Hoisted Mess
&lt;/h3&gt;

&lt;p&gt;When you run &lt;code&gt;npm install&lt;/code&gt; (or &lt;code&gt;yarn install&lt;/code&gt; with Yarn 1), the package manager looks at your &lt;code&gt;package.json&lt;/code&gt;, resolves all the dependencies and their dependencies and their dependencies' dependencies, and then writes them all into &lt;code&gt;node_modules&lt;/code&gt;. The thing is, it doesn't write them in a tree. It mostly flattens them.&lt;/p&gt;

&lt;p&gt;Why flatten? Historical reasons. In the very early days, Node had nested &lt;code&gt;node_modules&lt;/code&gt; folders. If your project depended on package A, and A depended on &lt;a href="mailto:B@1.0"&gt;B@1.0&lt;/a&gt;, and you also depended directly on &lt;a href="mailto:B@2.0"&gt;B@2.0&lt;/a&gt;, then you'd get a nested structure: &lt;code&gt;node_modules/B&lt;/code&gt; (2.0) and &lt;code&gt;node_modules/A/node_modules/B&lt;/code&gt; (1.0). This worked fine, but on Windows the path lengths would explode and break the file system. So npm 3 introduced &lt;strong&gt;hoisting&lt;/strong&gt; — pull as many packages as possible up to the top level of &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is why your &lt;code&gt;node_modules&lt;/code&gt; folder, when you peek inside it, has hundreds of packages at the top level even though your &lt;code&gt;package.json&lt;/code&gt; only lists twenty. All those extras are transitive dependencies that got hoisted up.&lt;/p&gt;

&lt;p&gt;And here is the bug that has eaten more of my Tuesdays than I want to remember: &lt;strong&gt;phantom dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Say your project depends on &lt;code&gt;express&lt;/code&gt;. Express depends on &lt;code&gt;debug&lt;/code&gt;. Both end up in your top-level &lt;code&gt;node_modules&lt;/code&gt;. Now in your code, you write &lt;code&gt;const debug = require('debug')&lt;/code&gt; even though &lt;code&gt;debug&lt;/code&gt; is not in your &lt;code&gt;package.json&lt;/code&gt;. It works! Because Node's resolution algorithm just looks for packages in &lt;code&gt;node_modules&lt;/code&gt;, it doesn't care whether you declared them. Your tests pass. You commit. Six months later, Express updates and drops &lt;code&gt;debug&lt;/code&gt; as a dependency. You upgrade Express. Everything explodes. And you have no idea why because the error says "cannot find module debug" and you're looking at code that's been working fine for half a year.&lt;/p&gt;

&lt;p&gt;This is a phantom dependency. It's a package you're using but not declaring. npm and Yarn Classic let you do this freely, by design. You can't really opt out without third-party tools.&lt;/p&gt;

&lt;p&gt;The other annoyance with the flat model is &lt;strong&gt;non-determinism in hoisting&lt;/strong&gt;. If two packages need conflicting versions of the same dependency, the package manager has to decide which one to hoist and which one to nest. Different versions of npm can make different decisions. Different lockfile states can make different decisions. This is part of why "delete &lt;code&gt;node_modules&lt;/code&gt; and reinstall" is JavaScript's universal cure-all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Yarn Berry: Plug'n'Play, the Beautiful Disaster
&lt;/h3&gt;

&lt;p&gt;Yarn 2+ took a completely different approach. They looked at &lt;code&gt;node_modules&lt;/code&gt; and said: this is wrong. The whole concept is wrong. Why are we making a giant folder full of files when we have a lockfile that knows exactly which version of each package every part of our code needs?&lt;/p&gt;

&lt;p&gt;So they killed &lt;code&gt;node_modules&lt;/code&gt;. With &lt;strong&gt;Plug'n'Play&lt;/strong&gt;, Yarn Berry stores all packages as zip files in &lt;code&gt;.yarn/cache/&lt;/code&gt;, and writes a single file called &lt;code&gt;.pnp.cjs&lt;/code&gt; (or &lt;code&gt;.pnp.loader.mjs&lt;/code&gt; for ESM) at the root of your project. This file is a giant lookup table that maps "package X at version Y, when imported by package Z" to "this exact zip file in this exact location." Yarn then patches Node's module resolution to consult this file instead of walking &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The advantages are real:&lt;/p&gt;

&lt;p&gt;Installs are faster, because there's no file I/O for thousands of small files — just unzipping when needed. Installs are deterministic, because the lookup table is deterministic. Phantom dependencies are impossible — if you &lt;code&gt;require&lt;/code&gt; something not in your &lt;code&gt;package.json&lt;/code&gt;, the lookup fails, full stop. Disk usage drops. You can even check &lt;code&gt;.yarn/cache/&lt;/code&gt; into git and have &lt;strong&gt;zero-installs&lt;/strong&gt;, where cloning the repo gives you a working project with no &lt;code&gt;yarn install&lt;/code&gt; step at all.&lt;/p&gt;

&lt;p&gt;The disadvantages are also real, and they killed PnP for a lot of teams.&lt;/p&gt;

&lt;p&gt;The whole JavaScript ecosystem assumes &lt;code&gt;node_modules&lt;/code&gt; exists. Tools assume it. Editors assume it. Webpack assumed it. TypeScript assumed it. ESLint assumed it. When PnP first came out, every other tool needed a Yarn plugin or a special configuration to work. The TypeScript integration required &lt;code&gt;yarn dlx @yarnpkg/sdks vscode&lt;/code&gt;, which set up VSCode to understand the PnP layout. If a colleague forgot that step, their editor would have red squiggles under every import while yours was fine. Some packages just didn't work with PnP at all, and you had to add them to a &lt;code&gt;packageExtensions&lt;/code&gt; field in &lt;code&gt;.yarnrc.yml&lt;/code&gt; to manually patch their dependencies.&lt;/p&gt;

&lt;p&gt;Yarn Berry also introduced a &lt;code&gt;node-modules&lt;/code&gt; linker as a fallback, so you could opt back into the old model. A lot of teams that "use Yarn Berry" actually use it in &lt;code&gt;nodeLinker: node-modules&lt;/code&gt; mode, which is basically Yarn Classic with a different lockfile format. You get none of the PnP benefits and a more complicated config.&lt;/p&gt;

&lt;p&gt;I have used Yarn Berry on three projects. Two of them were pure joy after the first week. One was a nightmare because we had a legacy dependency that did dynamic &lt;code&gt;require&lt;/code&gt; calls based on user input, and PnP couldn't follow them. We migrated that project off Yarn Berry six months later. The honest summary: Yarn Berry is technically excellent and culturally lonely.&lt;/p&gt;

&lt;h3&gt;
  
  
  pnpm: Symlinks All the Way Down
&lt;/h3&gt;

&lt;p&gt;pnpm is the one that, when you understand how it works, makes you go "wait, why didn't we do this from the start?"&lt;/p&gt;

&lt;p&gt;pnpm has two layers. First, there's a &lt;strong&gt;content-addressable store&lt;/strong&gt;, usually at &lt;code&gt;~/.local/share/pnpm/store&lt;/code&gt; (or &lt;code&gt;~/Library/pnpm/store&lt;/code&gt; on Mac). Every package version you've ever installed lives there exactly once. Not once per project — once on your entire machine. &lt;code&gt;lodash@4.17.21&lt;/code&gt; is a single set of files in the store, regardless of how many projects use it.&lt;/p&gt;

&lt;p&gt;Second, in each project, pnpm creates a &lt;code&gt;node_modules&lt;/code&gt; folder. But it's not flat. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/
├── express          → symlink to .pnpm/express@4.18.2/node_modules/express
├── react            → symlink to .pnpm/react@18.2.0/node_modules/react
└── .pnpm/
    ├── express@4.18.2/
    │   └── node_modules/
    │       ├── express/      → hard link to global store
    │       ├── debug/        → symlink to .pnpm/debug@4.3.4/...
    │       └── ...
    ├── debug@4.3.4/
    │   └── node_modules/
    │       └── debug/        → hard link to global store
    └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top level of &lt;code&gt;node_modules&lt;/code&gt; only contains your direct dependencies — the things in your &lt;code&gt;package.json&lt;/code&gt;. Each of those is a symlink into the &lt;code&gt;.pnpm/&lt;/code&gt; folder, which contains the real layout. Inside &lt;code&gt;.pnpm/&lt;/code&gt;, each package gets its own folder named with its version, and inside that folder is a &lt;code&gt;node_modules/&lt;/code&gt; containing the package itself (as a hard link to the global store) plus symlinks to its dependencies.&lt;/p&gt;

&lt;p&gt;This is brilliant for several reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phantom dependencies become impossible by default.&lt;/strong&gt; If you don't list &lt;code&gt;debug&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;, then there's no symlink to &lt;code&gt;debug&lt;/code&gt; at the top of your &lt;code&gt;node_modules&lt;/code&gt;. So &lt;code&gt;require('debug')&lt;/code&gt; from your code fails immediately. This is a feature, not a bug, and it has saved me from so many mystery breakages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk space is shared across projects.&lt;/strong&gt; If you have ten Node projects on your machine, you have one copy of each unique package version on disk. The global store on my laptop is currently 8.4 GB. The combined &lt;code&gt;node_modules&lt;/code&gt; of all my projects, if I were using npm, would be somewhere north of 60 GB. With pnpm, the actual on-disk size of all those &lt;code&gt;node_modules&lt;/code&gt; folders combined is closer to 12 GB, because most of it is hard links pointing to the same blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installs are fast because most of the work is hard-linking, not copying.&lt;/strong&gt; Hard-linking a file is essentially free — it's a couple of metadata operations. Copying a file requires actually reading and writing bytes. pnpm's "warm" install on a project I worked on (about 1,200 dependencies) took 6 seconds. The same &lt;code&gt;npm install&lt;/code&gt; took 45 seconds.&lt;/p&gt;

&lt;p&gt;The downsides of pnpm exist but are smaller than they used to be:&lt;/p&gt;

&lt;p&gt;Some packages assume the flat hoisted layout and break with strict pnpm. The fix is usually &lt;code&gt;public-hoist-pattern&lt;/code&gt; in &lt;code&gt;.npmrc&lt;/code&gt;, or in extreme cases &lt;code&gt;--shamefully-hoist&lt;/code&gt;, which makes pnpm behave like npm. The name is intentional: the maintainers want you to feel bad about using it, and you should.&lt;/p&gt;

&lt;p&gt;Symlinks on Windows used to be flaky. They're fine now on modern Windows 10 and 11 if developer mode is on, but you can still hit weird issues with certain antivirus tools or file watchers. On WSL, no problems.&lt;/p&gt;

&lt;p&gt;Some bundlers and build tools used to choke on the symlink structure. Webpack 4 had issues. Vite, esbuild, Rollup, Webpack 5 all handle it fine. If you're on a modern toolchain, you won't notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: Numbers, With Caveats
&lt;/h2&gt;

&lt;p&gt;Everyone benchmarks package managers wrong. They run &lt;code&gt;npm install&lt;/code&gt; once, then &lt;code&gt;pnpm install&lt;/code&gt; once, and call it a day. The reality is that there are like five different "install" scenarios, and the answer changes for each.&lt;/p&gt;

&lt;p&gt;Here's the rough picture, based on my own benchmarking on a project with 947 dependencies (a fairly chunky Next.js + tRPC + Prisma stack), running on a M3 MacBook with a fast SSD and a 1 Gbps connection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold install&lt;/strong&gt; (no lockfile, no cache, fresh &lt;code&gt;node_modules&lt;/code&gt;). This is the worst case — basically a brand new machine cloning the repo for the first time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm 10: 78 seconds&lt;/li&gt;
&lt;li&gt;yarn 1: 41 seconds&lt;/li&gt;
&lt;li&gt;yarn 4 (pnp): 23 seconds&lt;/li&gt;
&lt;li&gt;yarn 4 (node-modules): 38 seconds&lt;/li&gt;
&lt;li&gt;pnpm 9: 31 seconds&lt;/li&gt;
&lt;li&gt;bun 1.1: 9 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Warm install with lockfile&lt;/strong&gt; (lockfile exists, cache populated, &lt;code&gt;node_modules&lt;/code&gt; exists, nothing changed). This is what happens when CI runs and your dependencies haven't changed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm 10: 14 seconds&lt;/li&gt;
&lt;li&gt;yarn 1: 8 seconds&lt;/li&gt;
&lt;li&gt;yarn 4 (pnp): 1.4 seconds&lt;/li&gt;
&lt;li&gt;pnpm 9: 1.8 seconds&lt;/li&gt;
&lt;li&gt;bun 1.1: 0.6 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Warm install after deleting node_modules&lt;/strong&gt; (lockfile + cache, but node_modules is gone — the classic "delete and reinstall" workflow).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm 10: 22 seconds&lt;/li&gt;
&lt;li&gt;yarn 1: 18 seconds&lt;/li&gt;
&lt;li&gt;yarn 4 (pnp): 2 seconds (because there's no node_modules to recreate, just .pnp.cjs)&lt;/li&gt;
&lt;li&gt;pnpm 9: 6 seconds&lt;/li&gt;
&lt;li&gt;bun 1.1: 1.5 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adding a single dependency&lt;/strong&gt; (the most common everyday operation).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm 10: 11 seconds&lt;/li&gt;
&lt;li&gt;yarn 1: 7 seconds&lt;/li&gt;
&lt;li&gt;yarn 4: 3 seconds&lt;/li&gt;
&lt;li&gt;pnpm 9: 4 seconds&lt;/li&gt;
&lt;li&gt;bun 1.1: 0.8 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few things stand out. Bun is the fastest by a wide margin, when it works. (More on that.) pnpm and Yarn Berry trade blows depending on the scenario. npm is consistently the slowest, but the gap has narrowed dramatically since around npm 9 — npm has gotten genuinely better, and the days of &lt;code&gt;npm install&lt;/code&gt; taking minutes for medium projects are mostly over.&lt;/p&gt;

&lt;p&gt;The other dimension is &lt;strong&gt;disk usage&lt;/strong&gt;. On the same Next.js project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: 612 MB &lt;code&gt;node_modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;yarn 1: 597 MB&lt;/li&gt;
&lt;li&gt;yarn 4 (pnp): 89 MB &lt;code&gt;.yarn/cache&lt;/code&gt; + a tiny &lt;code&gt;.pnp.cjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pnpm: 198 MB project &lt;code&gt;node_modules&lt;/code&gt; (mostly hard links — actual disk usage is much lower)&lt;/li&gt;
&lt;li&gt;bun: 580 MB (bun uses a flat layout currently)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ten projects on your machine, multiply those numbers by ten and ask yourself how much of your SSD you want to dedicate to duplicates of TypeScript and Babel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The npm Story: Boring, Default, Mostly Fine Now
&lt;/h2&gt;

&lt;p&gt;I've been hard on npm in this article and I want to be fair. npm in 2026 is genuinely good. It's not the disaster it was in 2016. The team at GitHub (which has owned npm since 2020) has shipped real improvements:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;package-lock.json v3&lt;/code&gt; is more compact and reproducible than v1 was. Workspaces (added in npm 7) actually work for monorepos, even if they're less powerful than pnpm's. &lt;code&gt;npm audit&lt;/code&gt; is integrated into the install flow. &lt;code&gt;npm ci&lt;/code&gt; gives you a clean, deterministic install for CI environments. &lt;code&gt;npm exec&lt;/code&gt; (and the older &lt;code&gt;npx&lt;/code&gt;) handle one-off package execution.&lt;/p&gt;

&lt;p&gt;The biggest reason to use npm is that it's there. Every Node installation has it. Every tutorial assumes it. Every AI coding assistant defaults to it. If you're working on a small project, a quick prototype, a tutorial repo, or anything where "lowest common denominator" is the right move — npm is the right call. You will not regret it.&lt;/p&gt;

&lt;p&gt;Where npm starts to creak is when your project grows. The phantom dependency problem doesn't go away just because npm has gotten faster. Workspaces work but they're not as ergonomic as pnpm's. Disk usage grows linearly with project count. And the &lt;code&gt;package-lock.json&lt;/code&gt; merge conflicts in large teams are genuinely painful — that file regenerates wholesale on certain operations and produces godawful diffs.&lt;/p&gt;

&lt;p&gt;If your team is small and your projects are simple, just use npm. Don't let me or anyone else talk you into churn for the sake of churn. Switching package managers has costs, and they only pay off above a certain project size or team size.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Yarn Story: A Cautionary Tale About Forks
&lt;/h2&gt;

&lt;p&gt;Yarn's situation in 2026 is genuinely strange.&lt;/p&gt;

&lt;p&gt;Yarn 1 is still maintained but barely. The repo gets occasional security patches. Most of its features (parallel installs, lockfiles, offline cache) have been adopted by npm, so the original reason to switch is gone. If you're still on Yarn 1 today, it's probably because of inertia or because you have CI configurations that would be a pain to migrate.&lt;/p&gt;

&lt;p&gt;Yarn 4 (the latest Berry) is technically excellent. The plugin system is genuinely powerful. Constraints — a feature that lets you enforce rules across a workspace, like "all packages must use the same React version" — is fantastic for monorepos. Zero-installs are a real productivity win when they work.&lt;/p&gt;

&lt;p&gt;But the social proof has moved. When you go to a new TypeScript project's setup docs in 2026, the "recommended" package manager is almost always pnpm. Yarn Berry is the technically interesting choice that you need to defend in code review. "Why are we using Yarn Berry?" "Because we like it." "Okay but the rest of the ecosystem is on pnpm." "...okay."&lt;/p&gt;

&lt;p&gt;I'd describe Yarn Berry's current state as: if you're already on it and it's working, stay. The migration cost off it isn't worth the hypothetical benefits of switching. If you're starting a new project, pnpm gets you 80% of the benefits with 20% of the friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pnpm Story: How a Niche Tool Took Over
&lt;/h2&gt;

&lt;p&gt;pnpm's growth curve over the last four years has been quietly dramatic. Looking at the State of JS surveys: in 2021, around 16% of respondents had used pnpm. In 2023, that was up to 31%. The 2024 numbers showed pnpm passing Yarn in "would use again" satisfaction scores for the first time. By 2025, pnpm was essentially tied with npm for new monorepo adoption among teams over 20 engineers.&lt;/p&gt;

&lt;p&gt;Why? A few reasons.&lt;/p&gt;

&lt;p&gt;The first is that the technical advantages pnpm provides — strictness, disk efficiency, monorepo ergonomics — actually matter more as projects grow. For a five-page Next.js site, you don't care. For a 40-package monorepo with three apps and a shared component library and a couple of internal CLIs, you care a lot. And the JavaScript world has been steadily moving toward bigger monorepos, partly because tools like Turborepo and Nx have made them tractable.&lt;/p&gt;

&lt;p&gt;The second is that pnpm got good defaults. It picks up &lt;code&gt;.npmrc&lt;/code&gt; files, it speaks the same lockfile-ish language conceptually, and the migration from npm is usually &lt;code&gt;pnpm import&lt;/code&gt; (which converts &lt;code&gt;package-lock.json&lt;/code&gt; to &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;) and then &lt;code&gt;pnpm install&lt;/code&gt;. I've migrated four projects from npm to pnpm and the longest one took an afternoon, mostly spent fixing phantom dependencies that had been hiding for years.&lt;/p&gt;

&lt;p&gt;The third is that the major frameworks endorsed it. When Vue switched, when Vite shipped with pnpm-friendly examples, when SvelteKit's create script started recommending it — that's tens of thousands of new developers each month being introduced to pnpm as the default-feeling option.&lt;/p&gt;

&lt;p&gt;The fourth, and I think most underrated, is that pnpm's CLI ergonomics are just nicer. &lt;code&gt;pnpm add foo&lt;/code&gt; instead of &lt;code&gt;npm install foo&lt;/code&gt;. &lt;code&gt;pnpm dlx foo&lt;/code&gt; instead of &lt;code&gt;npx foo&lt;/code&gt;. &lt;code&gt;pnpm -r exec&lt;/code&gt; for running commands across all workspace packages. &lt;code&gt;pnpm why foo&lt;/code&gt; for tracing why a package is in your tree. These are small things but they add up.&lt;/p&gt;

&lt;p&gt;The pnpm pain points I've actually hit:&lt;/p&gt;

&lt;p&gt;Some old packages have implicit assumptions about hoisted dependencies and break under strict mode. The fix is usually a &lt;code&gt;public-hoist-pattern&lt;/code&gt; line in &lt;code&gt;.npmrc&lt;/code&gt;. About once a year a popular package needs this treatment and the pnpm community usually has the fix on their issue tracker within hours.&lt;/p&gt;

&lt;p&gt;The default behavior of running scripts in workspace packages is different from npm/yarn, and the syntax for things like "run build in only this package and its dependencies" is &lt;code&gt;pnpm --filter "...^my-app" build&lt;/code&gt;, which I had to look up the first three times I used it.&lt;/p&gt;

&lt;p&gt;The lockfile format is YAML, which is nicer to read than JSON but has YAML's classic indentation footguns when you try to manually edit it. (Don't manually edit lockfiles. I know. I've done it. It bit me.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Bun: The Wild Card
&lt;/h2&gt;

&lt;p&gt;I promised I'd come back to Bun. Bun is interesting because it's not really a package manager — it's a JavaScript runtime that happens to include a package manager that happens to be much faster than the others.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bun install&lt;/code&gt; on the same projects I benchmarked above was consistently 2-5x faster than pnpm and 5-10x faster than npm. This is partly because Bun is written in Zig and is genuinely well-optimized, and partly because Bun makes some opinionated choices (like its own lockfile format, &lt;code&gt;bun.lockb&lt;/code&gt;, which is binary) that trade compatibility for speed.&lt;/p&gt;

&lt;p&gt;The catch is that Bun is still maturing. Some packages that depend on Node's exact behavior have edge-case bugs under Bun. The binary lockfile is unreadable in code review (Bun added a text-based &lt;code&gt;bun.lock&lt;/code&gt; format in 2024 to address this). And while Bun aims to be a drop-in replacement, "drop-in replacement" is doing a lot of work in that sentence — there are real differences.&lt;/p&gt;

&lt;p&gt;For new projects in 2026, Bun is increasingly viable. I've shipped two side projects using &lt;code&gt;bun install&lt;/code&gt; and it's been fine. For an existing large project with complex dependencies and tooling, I'd be more cautious. The tooling around Bun (bundlers, deploy targets, etc.) is improving fast but isn't as mature as the npm/yarn/pnpm ecosystem.&lt;/p&gt;

&lt;p&gt;If Bun keeps its current trajectory, the "big four" of package managers in 2027 might genuinely be npm, yarn, pnpm, and bun, with bun taking meaningful share. Or it might fade like a dozen other "X but faster" projects have. I'm genuinely not sure which way it goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monorepos: The Killing Field
&lt;/h2&gt;

&lt;p&gt;Monorepos are where the differences between these tools stop being academic.&lt;/p&gt;

&lt;p&gt;In a monorepo, you have multiple packages in the same git repository — usually a couple of apps, a couple of libraries, and some shared tooling. The package manager has to know how to handle the relationships between these internal packages, install their external dependencies efficiently, and let you run scripts across them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm workspaces&lt;/strong&gt; are functional. &lt;code&gt;npm install&lt;/code&gt; from the root installs everything, internal dependencies get symlinked, scripts can be run with &lt;code&gt;npm run --workspace=foo build&lt;/code&gt;. It's fine for small monorepos. It starts to feel limited around the time you have 20+ packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yarn workspaces&lt;/strong&gt; were the original and are still solid. Yarn Berry's workspace tooling is genuinely powerful — constraints, focused workspaces (where you only install dependencies for one workspace), workspace-aware version bumping. If you're committed to Yarn Berry, the monorepo story is great.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pnpm workspaces&lt;/strong&gt; are widely considered the best of the three. The configuration is in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, the &lt;code&gt;--filter&lt;/code&gt; flag lets you target operations precisely (&lt;code&gt;pnpm --filter "@acme/web..." build&lt;/code&gt; builds the web app and everything it depends on), and the symlink-based store means duplicate dependencies across workspaces are deduplicated automatically. If you're starting a new monorepo today, the path of least resistance is pnpm + Turborepo or pnpm + Nx.&lt;/p&gt;

&lt;p&gt;I should be honest that for any of these, you probably want a monorepo task runner on top — Turborepo, Nx, or Lerna (Lerna is mostly maintenance mode but still works). The package manager handles installation; the task runner handles cached builds, parallel execution, and dependency-aware task graphs. The combination of pnpm + Turborepo has become the default for serious monorepos, and there's a reason for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: All Three Are About the Same
&lt;/h2&gt;

&lt;p&gt;This section is shorter than you'd expect. All three package managers have an &lt;code&gt;audit&lt;/code&gt; command. All three pull from the same npm registry, which means they're all subject to the same supply-chain risks. The notable JavaScript security incidents — event-stream in 2018, ua-parser-js in 2021, the colors/faker meltdown in 2022, the various typosquatting and dependency confusion attacks since — affected all three equally, because the root cause was always the registry, not the package manager.&lt;/p&gt;

&lt;p&gt;A few real differences:&lt;/p&gt;

&lt;p&gt;npm has had &lt;strong&gt;provenance&lt;/strong&gt; for a while, where packages can be cryptographically attested to come from a specific GitHub Actions build. This is useful but adoption is still spotty. Yarn and pnpm respect provenance metadata but the feature is npm-driven.&lt;/p&gt;

&lt;p&gt;pnpm's strictness gives a small security benefit: if a malicious package gets installed as a transitive dependency, you can't accidentally &lt;code&gt;require&lt;/code&gt; it from your own code, because phantom dependencies are blocked. This isn't a defense against the package being executed during install (that's a different attack surface), but it does limit the blast radius.&lt;/p&gt;

&lt;p&gt;Bun has the most aggressive security model in some ways — by default it doesn't run install scripts for unknown packages — but this also breaks some legitimate packages, so people often turn it off.&lt;/p&gt;

&lt;p&gt;If security is your concern, the package manager you pick matters less than: keeping dependencies up to date, running &lt;code&gt;audit&lt;/code&gt; in CI, using a tool like Renovate or Dependabot, pinning versions for production builds, and being skeptical of packages with one maintainer and three weekly downloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Should You Actually Use
&lt;/h2&gt;

&lt;p&gt;I have opinions. Here they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a solo project, prototype, tutorial repo, or anything throwaway:&lt;/strong&gt; npm. It's there. It works. Don't overthink it. The minutes you'd spend setting up pnpm are not coming back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a small team (under five engineers) on a single application:&lt;/strong&gt; npm or pnpm. If your team has any pnpm experience, pick pnpm — the disk savings and strictness are nice. If nobody has used it, the small productivity hit of switching probably isn't worth it for a project of this size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For a medium-to-large team on a single application:&lt;/strong&gt; pnpm. The strictness alone pays for itself the first time it catches a phantom dependency before production. The faster CI installs add up over thousands of CI runs. The disk savings on every developer's laptop add up too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For any monorepo:&lt;/strong&gt; pnpm, almost certainly with Turborepo on top. This is the closest thing to a no-brainer in this whole article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For an open-source library:&lt;/strong&gt; npm. Your contributors will arrive with npm installed. Don't make them learn another tool to fix a typo in your README. (You can use whatever you want internally; just make sure &lt;code&gt;npm install&lt;/code&gt; from the published package works for your users. It will, because the published artifact is just a tarball — the package manager you authored with doesn't propagate.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For an existing project on Yarn Classic:&lt;/strong&gt; Migrate to pnpm if it's painless (most of mine were). Stay on Yarn 1 if migration would touch a lot of CI scripts and the project is mature enough that "if it ain't broke" applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For an existing project on Yarn Berry:&lt;/strong&gt; Stay. Don't churn. The migration to pnpm will hit you in places you don't expect, and Yarn Berry is still actively maintained and excellent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Bun-curious folks:&lt;/strong&gt; Try it on a side project first. Don't migrate a production codebase to Bun without a real evaluation period.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stuff Nobody Tells You
&lt;/h2&gt;

&lt;p&gt;A few things I've learned from switching package managers more times than I should have:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lockfiles are not interchangeable.&lt;/strong&gt; If you switch from npm to pnpm, you cannot keep the &lt;code&gt;package-lock.json&lt;/code&gt; around "just in case." Pick one. Commit it. Delete the others. Having both &lt;code&gt;package-lock.json&lt;/code&gt; and &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; in the same repo is a recipe for confusion and divergent installs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engines field in &lt;code&gt;package.json&lt;/code&gt; is more important than you think.&lt;/strong&gt; &lt;code&gt;"engines": {"node": "&amp;gt;=20", "pnpm": "&amp;gt;=9"}&lt;/code&gt; plus &lt;code&gt;"packageManager": "pnpm@9.7.0"&lt;/code&gt; plus a Corepack-aware setup means new contributors get the right tooling without reading the README. Half the "it doesn't work on my machine" issues I've seen were someone running &lt;code&gt;npm install&lt;/code&gt; on a project that expected &lt;code&gt;pnpm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corepack is your friend.&lt;/strong&gt; Corepack ships with Node 16+ and lets you specify which package manager (and version) a project uses, so contributors don't need to install pnpm or yarn globally. &lt;code&gt;corepack enable&lt;/code&gt; once, and projects just work with their declared package manager. This has been around for years and is still under-used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.npmrc&lt;/code&gt; is read by all of them.&lt;/strong&gt; Most config options work across npm, yarn, and pnpm because pnpm and yarn both read &lt;code&gt;.npmrc&lt;/code&gt;. Your registry config, auth tokens, scoped registry settings — these usually port over without changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI is where speed actually matters.&lt;/strong&gt; On your laptop, an extra 30 seconds of install time is annoying. On CI, where every PR triggers an install, it's hundreds of dollars a month and minutes added to feedback loops. Use lockfile-aware installs (&lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;yarn install --frozen-lockfile&lt;/code&gt;, &lt;code&gt;pnpm install --frozen-lockfile&lt;/code&gt;) in CI. Cache the package manager's store, not &lt;code&gt;node_modules&lt;/code&gt;. The numbers add up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't put package manager opinions in code reviews.&lt;/strong&gt; I have seen so many PRs where someone added a feature and someone else commented "btw should we move to pnpm" and then the PR derailed for two weeks. Have the conversation in a separate channel. Don't make every feature PR a referendum on tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Conclusion
&lt;/h2&gt;

&lt;p&gt;I started this article saying that I stopped caring about package managers and then started caring again. The truth is somewhere in between.&lt;/p&gt;

&lt;p&gt;You should care enough to pick the right tool for your project size. You should not care so much that you spend a sprint migrating tools when you should be shipping features. The differences are real but they are not life-or-death. A team using npm with discipline will outship a team using pnpm with chaos every single time.&lt;/p&gt;

&lt;p&gt;If I had to compress this whole article into one sentence, it would be: &lt;strong&gt;use pnpm for anything serious, use npm for anything quick, watch Bun closely, and never touch Yarn Classic for new work.&lt;/strong&gt; That is, in 2026, my opinionated take. In 2028 it'll probably be different. The JavaScript ecosystem rewrites itself every five years and that's both its biggest weakness and the reason I still find it interesting to write code in.&lt;/p&gt;

&lt;p&gt;The two-and-a-half gigabyte &lt;code&gt;node_modules&lt;/code&gt; from the start of this article? After we migrated that project to pnpm with proper workspace structure, it dropped to 380 megabytes of project-specific links pointing into the global store. The first install on a new laptop went from ninety seconds to twenty-two. CI install times dropped from ninety seconds to fourteen seconds, on average, across about 40,000 builds per month. Nobody on the team has noticed. That's the point. Good tooling is invisible. The best package manager is the one that lets you forget there's a package manager at all.&lt;/p&gt;

&lt;p&gt;Now go finish whatever you were procrastinating on by reading this.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>devops</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in React Context API</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Fri, 27 Mar 2026 15:21:49 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-react-context-api-57pp</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-react-context-api-57pp</guid>
      <description>&lt;p&gt;Artificial intelligence has become very good at producing React code that looks convincing. Give it a prompt, mention Context API, and within seconds it can generate a provider, a custom hook, and a clean enough consumer structure to pass a quick review.&lt;/p&gt;

&lt;p&gt;That speed is impressive, but it is also deceptive.&lt;/p&gt;

&lt;p&gt;React Context is one of those tools that appears simple until the surrounding reality of a production application begins to matter. The moment you move beyond surface level implementation and start thinking about component boundaries, render cost, state modeling, accessibility, debugging, and migration, the conversation changes. At that point, the issue is no longer whether AI can generate working code. The issue is whether it can make the right architectural decisions.&lt;/p&gt;

&lt;p&gt;In practice, that is still where human judgment matters most.&lt;/p&gt;

&lt;p&gt;This is not because AI is useless. It is because Context is not just a syntax feature. It is an architectural mechanism. When used well, it reduces friction and clarifies ownership. When used poorly, it spreads cost and confusion across an entire application.&lt;/p&gt;

&lt;p&gt;Here are five things AI still struggles to do, even when React Context API is part of the solution.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It cannot decide where Context should begin and where it should stop&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the most common mistakes in React applications is not a broken Context implementation. It is an unnecessary one.&lt;/p&gt;

&lt;p&gt;AI tends to see shared data and immediately treat Context as the answer. A theme value becomes Context. Session data becomes Context. Modal state becomes Context. Notifications become Context. Filters, tabs, loading flags, and form state soon follow. Before long, the application starts to resemble a storage unit where unrelated concerns have been placed side by side simply because they might be needed somewhere else.&lt;/p&gt;

&lt;p&gt;That is rarely good design.&lt;/p&gt;

&lt;p&gt;The real challenge with Context is not creating it. The real challenge is drawing a boundary around what genuinely deserves to be shared across a subtree and what should remain local, explicit, and easier to reason about through props or composition.&lt;/p&gt;

&lt;p&gt;Human developers are still better at noticing when a piece of state only feels global because the component structure is messy. In many cases, the right fix is not Context at all. It is a clearer component boundary, a better parent child relationship, or a simpler data flow.&lt;/p&gt;

&lt;p&gt;AI often optimizes for convenience. Humans still have to optimize for clarity.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It cannot truly understand the structural cost of a provider&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A provider is never just a wrapper. Its position in the tree affects how state is distributed, how updates propagate, and how difficult the application becomes to reason about over time.&lt;/p&gt;

&lt;p&gt;This is where AI often falls short. It can generate a provider and consumer pair without difficulty, but it usually treats them as isolated code fragments. It does not naturally reason about the full topology of the component tree in the way an experienced engineer does.&lt;/p&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;A provider placed too high can cause broad and unnecessary subscriptions. A provider placed in the wrong branch can make data ownership unclear. A provider whose value object is recreated on every render can trigger update cascades that seem invisible at first but become expensive later. A provider that mixes unrelated concerns in one value may work perfectly in the beginning and quietly become a maintenance problem six months later.&lt;/p&gt;

&lt;p&gt;None of this is obvious from a generated snippet.&lt;/p&gt;

&lt;p&gt;The code may compile. The UI may behave correctly. Yet the structure underneath may already be wrong.&lt;/p&gt;

&lt;p&gt;That is why Context design remains a human task. It requires thinking in terms of the tree, not just the file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It cannot model state meaningfully as well as it models syntax&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a large difference between storing state and understanding state.&lt;/p&gt;

&lt;p&gt;Context is very good at making values available lower in the tree. It is not, by itself, a guarantee that the values inside it have been modeled correctly. Once applications become more complex, the hard problem is no longer distribution. It is meaning.&lt;/p&gt;

&lt;p&gt;Imagine a session object that contains the current user. From that session, you derive whether the user is authenticated. Then perhaps feature flags influence what the user is allowed to see. Then permissions shape what actions are available in the interface. At that point, the central question is not how to expose the data. It is which value is the source of truth, which values should be derived, and where that derivation should happen.&lt;/p&gt;

&lt;p&gt;AI often blurs those layers.&lt;/p&gt;

&lt;p&gt;It may store source state and derived state together in the same context value. It may duplicate the same business logic in multiple consumers. It may calculate important meaning in the provider itself, even when that logic should live in a dedicated hook or a more focused abstraction.&lt;/p&gt;

&lt;p&gt;That kind of design can survive for a while. The application still runs. The UI still appears correct. But the structure becomes fragile. Sooner or later you get inconsistencies that are difficult to explain, such as a user object being null while an authentication flag still says true, or two screens interpreting the same state differently.&lt;/p&gt;

&lt;p&gt;The more important the logic becomes, the more dangerous that drift is.&lt;/p&gt;

&lt;p&gt;AI is very good at producing shapes that resemble solutions. Humans are still better at protecting the internal truth of a system.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It cannot instinctively protect you from Context performance traps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Performance problems in React are rarely caused by one dramatic mistake. More often, they come from small design decisions that seemed harmless at the time.&lt;/p&gt;

&lt;p&gt;Context is especially vulnerable to this.&lt;/p&gt;

&lt;p&gt;A provider value that changes identity too often can trigger broad re renders. A large context that bundles fast changing and rarely changing values together can force unrelated consumers to update. A context that acts as a catch all store can spread render pressure through wide parts of the application even though only one small field has changed.&lt;/p&gt;

&lt;p&gt;This is where AI often sounds confident and remains shallow.&lt;/p&gt;

&lt;p&gt;Mention a rerender issue and it may quickly recommend memoization. That sounds reasonable, but it often fails to address the real problem. In many Context related cases, the issue is not whether a child is memoized. The issue is that the provider value itself is unstable, or that the shape of the context is too broad, or that the update frequency of the stored values makes the design unsuitable.&lt;/p&gt;

&lt;p&gt;In other words, the architecture is wrong before the optimization strategy even begins.&lt;/p&gt;

&lt;p&gt;An experienced developer usually responds differently. Instead of asking how to patch the rerender, they ask why this state is in Context at all, how often it changes, how many components subscribe to it, whether state and dispatch should be separated, whether the provider value should be stabilized, or whether another state management approach would fit the problem better.&lt;/p&gt;

&lt;p&gt;That diagnostic instinct still belongs mostly to humans.&lt;/p&gt;

&lt;p&gt;AI can suggest remedies. It is much less reliable at identifying the true source of the disease.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It cannot own the consequences of debugging, migration, and maintenance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first version of Context is rarely the hard part. The hard part arrives later, when something subtle breaks and the failure is spread across multiple layers of the application.&lt;/p&gt;

&lt;p&gt;A consumer unexpectedly reads a fallback value. A provider override deep in the tree changes behavior only on one screen. A React version upgrade introduces a rendering difference that was never visible before. A bundling issue duplicates modules and causes Context identity to behave strangely even though the code looks correct.&lt;/p&gt;

&lt;p&gt;These are not beginner problems. They are engineering problems.&lt;/p&gt;

&lt;p&gt;And this is precisely where AI becomes least dependable.&lt;/p&gt;

&lt;p&gt;Debugging Context requires more than pattern recognition. It requires tracing provenance. Which provider is supplying this value. Where is it being overridden. Why is this consumer seeing a different result than another one. Is the issue in the component tree, the module graph, the build output, or the migration path.&lt;/p&gt;

&lt;p&gt;AI can offer plausible guesses, but it cannot truly hold the lived context of your codebase in the way a human maintainer can. It does not own the repository history. It does not remember why the provider was placed there in the first place. It does not feel the weight of a bad migration choice that will cost your team weeks of cleanup later.&lt;/p&gt;

&lt;p&gt;This becomes even more important during framework upgrades. Context heavy areas are often sensitive to subtle behavioral differences. What looked stable under one version may suddenly require retesting, restructuring, or more careful profiling under another. AI can tell you what changed in general terms. It cannot responsibly judge the pressure points of your specific application without human verification.&lt;/p&gt;

&lt;p&gt;And that verification is not a formality. It is the work.&lt;/p&gt;

&lt;p&gt;Why this matters more than people admit&lt;/p&gt;

&lt;p&gt;The discussion around AI in development is often framed the wrong way. People ask whether AI can write React code. That question is already outdated. Of course it can.&lt;/p&gt;

&lt;p&gt;The better question is whether AI can make architectural decisions under uncertainty, with incomplete visibility, and with long term consequences in mind.&lt;/p&gt;

&lt;p&gt;React Context is a very good test of that question because it sits exactly at the border between code generation and system design.&lt;/p&gt;

</description>
      <category>react</category>
      <category>contentwriting</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Do you know these?</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 21 Mar 2026 01:21:50 +0000</pubDate>
      <link>https://future.forem.com/devunionx/do-you-know-these-2g43</link>
      <guid>https://future.forem.com/devunionx/do-you-know-these-2g43</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-recoil-4ela" class="crayons-story__hidden-navigation-link"&gt;5 Things AI Can't Do, Even in • Recoil&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/devunionx" class="crayons-avatar  crayons-avatar--l  "&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/devunionx" class="crayons-story__secondary fw-medium m:hidden"&gt;
              DevUnionX
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                DevUnionX
                
              
              &lt;div id="story-author-preview-content-3378529" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/devunionx" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;DevUnionX&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-recoil-4ela" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 21&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-recoil-4ela" id="article-link-3378529"&gt;
          5 Things AI Can't Do, Even in • Recoil
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/recoil"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;recoil&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-recoil-4ela" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/devunionx/5-things-ai-cant-do-even-in-recoil-4ela#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            12 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>recoil</category>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Recoil</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sat, 21 Mar 2026 01:20:28 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-recoil-4ela</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-recoil-4ela</guid>
      <description>&lt;h1&gt;
  
  
  5 Things AI Still Can't Do Even With Recoil
&lt;/h1&gt;

&lt;p&gt;I wrote this report based on Recoil's (for React) atom to selector based data flow graph approach. The goal was showing concrete limits through Recoil-specific technical nuances rather than staying at slogans like "AI writes code but can't design". Recoil official documentation speaks of a data graph flowing from atoms to selectors and selectors being able to do sync/async transformations. citeturn8view1 The five places where AI struggles in this technical framework all come down to the same root in my view: lack of intent and context.&lt;/p&gt;

&lt;p&gt;As of March 20, 2026, there's another critical background affecting the table: Recoil's most current release note is 0.7.7 from March 1, 2023, containing various SSR/Suspense issues and possible "unhandled promise rejection" fix for useRecoilCallback. citeturn10view2 Additionally Recoil GitHub repository was archived (read-only) on January 1, 2025. citeturn10view0 This further weakens the assumption that "AI already knows everything" because AI assistants mostly don't account for current maintenance status and ecosystem risk when generating code.&lt;/p&gt;

&lt;p&gt;The report's five main findings can be summarized like this. When component to state composition isn't properly structured, Recoil atom keys, boundaries (RecoilRoot), shared state versus local state distinction quickly gets out of control. citeturn8view1turn10view0 In atom/selector design, especially with atomFamily/selectorFamily normalization, small key or cache choice can return as "slowdown" or "memory leak" in real projects. citeturn7view0turn7view1turn8view2 Async selectors and atom effects, especially async calling of setSelf, can lead to race conditions and difficult bugs like "later value overwrote user change". citeturn4view1turn8view0 On debugging/snapshot/time-travel side, snapshot lifetime management with retain/release and "API under development" warnings are details AI frequently misses causing problems directly in production. citeturn6view1turn6view2turn6view0 In performance and large-scale compatibility, reading large collections through families in loops can lock CPU, plus memory management of some patterns is questionable. AI can produce these as "works" and leave it. citeturn7view0turn7view2turn7view3&lt;/p&gt;

&lt;p&gt;For preparing this report, I first examined Recoil's official documentation including Core Concepts, atom/selector APIs, dev tools and snapshot pages, Recoil blog release notes, and issues in Recoil GitHub repository. citeturn8view1turn8view3turn6view2turn10view2turn10view0 To understand Recoil's async selector patterns, I additionally utilized case writings focused on Recoil and community examples. citeturn0search20turn0search18turn0search4 In accessibility section, I didn't treat Recoil as "direct a11y tool". Instead I related how state changes create risk on UI/focus/announcement (live region) side with WCAG and WAI-ARIA guides. citeturn2search3turn3search1turn3search0 Finally to contextualize AI code assistant error modes, I referenced current research on AI agent PR analysis and software supply chain risks like package hallucination and slopsquatting. citeturn11search6turn11search5turn11search0&lt;/p&gt;

&lt;p&gt;Connected corporate document source wasn't provided this session, so I proceeded only with publicly available web sources. Therefore I didn't make claims like "says so in internal docs".&lt;/p&gt;

&lt;p&gt;Following flow summarizes where I see AI as accelerator versus risk multiplier in Recoil project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Product requirement and boundaries: local state or Recoil?] --&amp;gt; B[Draft with AI: atom/selector, atomFamily/selectorFamily suggestions]
  B --&amp;gt; C{Atom/selector key strategy consistent?}
  C -- No --&amp;gt; D[Create key dictionary and naming standard] --&amp;gt; B
  C -- Yes --&amp;gt; E{Should derived state really be selector?}
  E -- No --&amp;gt; F[Separate local state / memo / computation layer] --&amp;gt; B
  E -- Yes --&amp;gt; G{Async flow exists? (Promise selector, atom effects, SSR/Suspense)}
  G -- Yes --&amp;gt; H[Test race/cancel/retain rules, validate SSR target] --&amp;gt; B
  G -- No --&amp;gt; I{Performance and memory review: family+loop, leak risk}
  I -- Problem exists --&amp;gt; J[Change scaling pattern, apply cache policy &amp;amp; splitting] --&amp;gt; B
  I -- Clean --&amp;gt; K{a11y: focus, live region, modal behavior correct?}
  K -- No --&amp;gt; L[WAI-ARIA/WCAG check: focus trap + aria-live + keyboard flow] --&amp;gt; B
  K -- Yes --&amp;gt; M[Code review + test + prod observation]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technical background: Recoil builds data graph flowing from atoms to selectors. Atoms are subscribable state units and selectors transform this state sync or async. citeturn8view1turn8view0 Official documentation says atoms "can be used instead of React local component state", meaning technically you can atomize everything. citeturn8view1 This point moves design decision from "library" to "architecture". Whether a state will be shared (atom), derived (selector), or component-specific (local state) requires reading product behavior more than generating code.&lt;/p&gt;

&lt;p&gt;AI's concrete error modes: AI most frequently shows "let's write atom for everything" reflex in Recoil world. This produces two easy but dangerous results: (1) atom keys become soup, (2) application's "semantic boundaries" disappear (which state is UI state, which is domain state?). Recoil requires atom keys to be globally unique and these keys get used in places like debugging/persistence. Same key in two atoms gets accepted as error. citeturn8view1turn8view3 AI leaning toward Date.now or random IDs saying "let's generate key" undermines key's need to be "stable across executions" especially in persistence/debug scenarios. Recoil documentation emphasizes selector key (and similarly atom key) needs to be unique across entire application and stable for persistence. citeturn8view0turn8view3&lt;/p&gt;

&lt;p&gt;Additionally Recoil tries protecting atom/selector values from direct mutation to detect change correctly. If object held in atom or selector gets directly mutated, might not notify subscribers correctly, so freezes value objects in development mode. citeturn8view0turn8view3 AI using doors like dangerouslyAllowMutability as "quick fix" suppresses error short term but produces ugliest failures like "sometimes doesn't render" long term.&lt;/p&gt;

&lt;p&gt;Short code examples showing good versus bad. Good example with keys module-level constant and meaningful. Export const cartItemCountAtom equals atom number with key cart/itemCount comment persistent, unique, carries meaning, and default 0. Bad example with different key every run causing debug/persist nightmare. Export const cartItemCountAtom equals atom number with key cart/itemCount plus Date.now comment key not stable, and default 0.&lt;/p&gt;

&lt;p&gt;Good example moving expensive state read to action not render. useRecoilCallback provides reading through snapshot without subscribing during render. citeturn9view2 Import useRecoilCallback from recoil. Function CartDebugButton. Const dumpCart equals useRecoilCallback with snapshot parameter async arrow function. Comment reading on click not render time. Const count equals await snapshot.getPromise cartItemCountAtom. Console.log Cart count. Return button onClick dumpCart showing Log Cart.&lt;/p&gt;

&lt;p&gt;Mitigation strategies for developer: I treat Recoil key management like "hidden API", extracting a key dictionary, establishing naming standard like domain/entity/field, first place I look in code review when adding new atom is key. To not mutate atom/selector objects, I make immutable update habit into "rule", instead of turning off freeze warnings in development mode, I fix point where warning appears. citeturn8view3turn8view0 If expensive read needed during render, I prefer useRecoilCallback or on-demand snapshot approach because Recoil explicitly warns tools like useRecoilSnapshot can trigger re-render at every state change. citeturn9view0turn9view2&lt;/p&gt;

&lt;p&gt;Technical background: Atoms are "source of truth", selectors are "derived state". Selectors should be thought of as pure/side-effect-free function. citeturn8view1turn8view0 Recoil officially provides pattern with selectorFamily that takes parameter and returns same memoized selector instance for same parameter, very powerful for normalization and "ID-based access". citeturn8view2 Despite this, normalization itself, which atomFamily, which selectorFamily, which key schema, is modeling decision not technical. When done wrong, returns not just "wrong value" but "slowness" and "memory bloat".&lt;/p&gt;

&lt;p&gt;AI's concrete error modes: AI has two typical extremes here. (1) making everything atomFamily using like "as if DB", (2) conversely keeping everything in single atom trying to fragment with selectors. When large collections have atomFamily/selectorFamily designed wrong, there are early-period issues saying resources on Recoil side aren't released. For instance one issue reports "resources created with atomFamily/selectorFamily aren't properly freed after unmount". citeturn7view2 Parallel to this, "memory management/garbage collection" topic also discussed in Recoil issues under separate headings. citeturn7view3 AI without knowing this history and discussions can present family plus large list combination as default solution saying "works anyway".&lt;/p&gt;

&lt;p&gt;More dangerous: using selector for "lazy init" of atom default. Atom API documentation says default can be selector and if selector default used, atom's default can dynamically update as default selector updates. citeturn8view3 But in real world, there's closed issue saying "memory leak when atom default is selector". This issue claims making atom default lazy with selector "caches all old set values" leading to memory leak. Proposed fix even touches eviction strategy with cachePolicy_UNSTABLE. citeturn7view1 AI cannot be expected to catch such "exists in docs but turned out problematic in practice" distinctions.&lt;/p&gt;

&lt;p&gt;Short code examples showing normalization plus error. Good example with ID-based atomFamily plus derived selectorFamily. Import atomFamily and selectorFamily from recoil. Type User with id, name, teamId optional. Export const userByIdAtom equals atomFamily User or null, string with key entities/userById and default null. Export const userNameSelector equals selectorFamily string, string with key derived/userName and get with id parameter showing get function with user equals get userByIdAtom id, return user?.name or (nameless). Comment: Atom source of truth, selector derived state, separation clear. citeturn8view2&lt;/p&gt;

&lt;p&gt;Risky pattern making atom default lazy init with selector. Some scenarios reported memory leak. citeturn7view1 Import atom and selector from recoil. Export const transactionsAtom equals atom with key transactions and default selector with key transactions/default and get async arrow returning await retrieveTransactions. Comment showing lazy init intent.&lt;/p&gt;

&lt;p&gt;Mitigation strategies for developer: I put two safety belts in atom/selector design. First, I do normalization "little but correct", using families only when really need ID-based sharing. Patterns like "pulling thousands of items with loop in selectorFamily" I put in early performance test because this locking CPU was reported with real issue. citeturn7view0 Second, in controversial patterns like making atom default lazy with selector, I shift to atom effects or more controlled loading strategies. Recoil explains atom effects specifically with "putting policy into atom definition" motivation and allows managing side effects by returning cleanup handler. citeturn4view1&lt;/p&gt;

&lt;p&gt;Technical background: Recoil selectors can be sync or work async by returning Promise. Documentation positions selectors as "idempotent/pure function" but also explicitly shows get function can return Promise. citeturn8view0 On atom side, default value can be Promise or async selector, in this case atom becomes "pending" and can trigger Suspense. citeturn8view3turn10view2 This is nice but in practice all questions like "when will fetch trigger", "how will it cache", "what happens in SSR" are design decisions.&lt;/p&gt;

&lt;p&gt;AI's concrete error modes: Even in simplest scenario like do I fetch data on render or on button click, there are different patterns in Recoil ecosystem. For instance in Recoil issue about data fetching scenario with web API, two options asked: (a) do fetch on onClick writing result to atom, or (b) onClick changes an atom and selector reloads through dependency? citeturn5view2 This question alone makes it hard for AI to produce "one correct answer" because answer depends on product behavior like prefetch, idempotent, user action, rate-limit. AI mostly chooses pattern (b) because "reactive", but this can lead to selector triggering again and again as atom changes and unintentionally raining API calls.&lt;/p&gt;

&lt;p&gt;On atom effects side, Recoil officially supports policies like persistence/sync/history with setSelf and onSet. citeturn4view1 But there's subtle landmine here: async setSelf call, according to docs, can overwrite atom's value if comes after atom set by user. citeturn4view1 In "async init plus user interaction" codes AI produces, you mostly see this race condition. User fills form, then old state from localForage arrives and rewinds form. This isn't Recoil's fault, it's design error.&lt;/p&gt;

&lt;p&gt;On snapshot side also async subtle detail: Snapshot documentation says snapshots held during callback duration, retain needed for async usage, also warns "async selectors can be canceled if not actively used, must retain if looking with snapshot". citeturn6view2 AI assistants frequently skip this retain requirement in codes wanting to resolve async selector with snapshot to see resolved value. Result becomes hair-pulling failure like "sometimes comes sometimes doesn't".&lt;/p&gt;

&lt;p&gt;Short code examples showing async selector plus atom effect. Async selectorFamily fetching with ID parameter, same parameter gets memoized. citeturn8view2turn8view0 Import selectorFamily from recoil. Export const userQuery equals selectorFamily with key queries/user and get with id string parameter async arrow. Const res equals await fetch /api/users/${id}. If not res.ok throw new Error User couldn't be fetched. Return await res.json.&lt;/p&gt;

&lt;p&gt;Warning atom effect with async setSelf showing watch for race condition. According to docs "if atom set, later setSelf can overwrite". citeturn4view1 Import atom from recoil. Export const settingsAtom equals atom with key settings, default object theme light, effects array with setSelf, onSet, trigger parameters. If trigger equals get, comment persisted value coming late can overwrite user change. setTimeout with setSelf object theme dark after 1000ms. onSet with newValue arrow, comment persistent write etc. localStorage.setItem settings with JSON.stringify newValue.&lt;/p&gt;

&lt;p&gt;Mitigation strategies for developer: I apply two principles in async state. (1) selectors should be pure, if side effect needed move to atom effects or UI layer event handler, (2) "async init" should always be written with assumption "can overwrite user interaction". I take seriously trigger usage in atom effects documentation, especially limiting expensive init with trigger equals get, and don't neglect cleanup handlers like unsubscribe. citeturn4view1 If fetch needed on user action instead of render, I prefer onClick with useRecoilCallback for snapshot read plus set/refresh pattern because this hook designed with motivation of async reading without render-time subscription. citeturn9view2&lt;/p&gt;

&lt;p&gt;Technical background: Recoil officially presents "observe all state changes" and inspect through snapshot approach. Dev tools guide says you can subscribe to state changes with useRecoilSnapshot or useRecoilTransactionObserver_UNSTABLE getting snapshot, also puts warning "API under development, will change". citeturn6view1turn6view2 Snapshot API page also notes this area evolving with _UNSTABLE emphasis. citeturn6view2 For time travel, useGotoRecoilSnapshot recommended. citeturn9view1turn6view1&lt;/p&gt;

&lt;p&gt;AI's concrete error modes: Most typical AI error here: copying time-travel example from docs and storing snapshots inside React state. Problem is snapshot lifetime is limited. Time-travel issue on GitHub says keeping snapshot outside callback duration gives warning and "if you'll keep snapshot long, do retain/release" sharing example code. citeturn6view0turn6view2 AI assistant generally doesn't see this warning because example appears to "work", but even issue text mentions will turn into exception later. citeturn6view0&lt;/p&gt;

&lt;p&gt;Another error is using useRecoilSnapshot everywhere in application for debug. Recoil documentation explicitly says this hook will re-render component at all Recoil state changes and asks to be careful. citeturn9view0 AI can suggest "put debug observer everywhere", then unnecessary render storm starts in prod. Additionally useGotoRecoilSnapshot page notes transaction example is inefficient because subscribes "all state changes". citeturn9view1turn9view0&lt;/p&gt;

&lt;p&gt;Short code examples showing correct time travel. If I'll store snapshot, I comply with retain/release discipline. citeturn6view2turn6view0 Import useEffect and useRef from react, useRecoilSnapshot and useGotoRecoilSnapshot from recoil. Export function UndoRedo. Const snapshot equals useRecoilSnapshot. Const gotoSnapshot equals useGotoRecoilSnapshot. Comment holding retained snapshots here. Const historyRef equals useRef array type. useEffect with const release equals snapshot.retain comment extending snapshot lifetime. historyRef.current.push object snap snapshot and release. Comment example 50 step limit. If historyRef.current.length greater than 50, const first equals historyRef.current.shift, first?.release. Return arrow function comment releasing all when component closes. historyRef.current.forEach h arrow h.release. historyRef.current equals empty array. With snapshot dependency. Return button onClick showing last equals historyRef.current.at(-2) comment previous one, if last gotoSnapshot last.snap showing Undo.&lt;/p&gt;

&lt;p&gt;Debug state dump with "on-demand snapshot" approach. Dev tools guide and useRecoilCallback recommend this. citeturn6view1turn9view2 Import useRecoilCallback from recoil. Function DumpStateButton. Const dump equals useRecoilCallback with snapshot parameter async arrow. console.debug Atom/selector dump starting. For const node of snapshot.getNodes_UNSTABLE. Const value equals await snapshot.getPromise node. console.debug node.key value. Return button onClick dump showing State Dump.&lt;/p&gt;

&lt;p&gt;Mitigation strategies for developer: I design debugging tools like snapshot observer and time travel for "dev" not "prod", taking seriously "under development" note in docs putting durable abstraction layer (adapter) against version changes. citeturn6view1turn6view2 If I'll store snapshots, I make retain/release discipline into code standard, otherwise "ghost snapshot" and memory pressure can happen over time. citeturn6view2turn6view0 Additionally I use feature flag or env guard to not put hooks subscribing to all state changes (especially useRecoilSnapshot) into prod bundle. citeturn9view0turn9view1&lt;/p&gt;

&lt;p&gt;Technical background: Recoil's performance promise relies on "render as needed" idea with atom-based subscription and selector re-evaluations. Core Concepts says when atom updated, components subscribed to that atom re-render. citeturn8view1 Selector documentation also explains selector re-evaluates when dependency changes and additionally warns "if you mutate selector value object, subscriber notification can be bypassed". citeturn8view0 So performance comes when application modeling done right, when done wrong Recoil isn't "magic".&lt;/p&gt;

&lt;p&gt;AI's concrete error modes regarding performance and memory: In large collections, getting a bunch of IDs one by one with get in loop with selectorFamily can hit CPU to 100% locking main thread in practice. Developer experiencing this opened issue saying "computation takes very long with big array of ids, CPU 100%" sharing example code. citeturn7view0turn8view2 AI can suggest this pattern as "very clean" but problem explodes when scale grows.&lt;/p&gt;

&lt;p&gt;Memory side has similar reality: there's issue claiming atomFamily/selectorFamily resources not freed at unmount and key changes, containing strong suggestion like "should be marked _UNSAFE". citeturn7view2 Memory leak reports exist in pattern making atom default lazy init with selector. Issue explains this leak relates to cache behavior and eviction suggestion made with cachePolicy_UNSTABLE. citeturn7view1turn8view0 These don't mean "Recoil is bad" but mean "wrong pattern, wrong cache policy, wrong scale assumption". AI mostly thinks "copy-paste scales".&lt;/p&gt;

&lt;p&gt;Ecosystem compatibility for large scale question: Real risk in big project isn't just performance but maintenance and compatibility. Recoil repo being archived and last release note staying in 2023 affects long-term maintenance plan. citeturn10view0turn10view2 In such situation, AI assistant saying "latest Recoil feature" can suggest actually controversial/_UNSTABLE API causing you to lock into it in prod.&lt;/p&gt;

&lt;p&gt;Additionally AI's another scale risk in supply chain: models generating code can suggest hallucinated package names and this creating "package confusion" attack surface was examined in detail in large-scale study. citeturn11search0turn11search4 In Recoil ecosystem also third-party tools exist like recoil-persist and recoilize. AI can suggest non-existent package as if exists. This is why I don't accept "package AI suggested equals automatically correct".&lt;/p&gt;

&lt;p&gt;Short code examples showing performance and safety belt. Warning large lists plus selectorFamily loop reported as "CPU 100%" in issues. citeturn7view0 Comment I definitely measure this type pattern with profiler. Const resourcesState equals selectorFamily with key resourcesState and get with ids string array parameter showing get function with ids.map id arrow get resourceState id.&lt;/p&gt;

&lt;p&gt;Good example consciously choosing cache policy, passes in issue as leak fix suggestion. citeturn7view1turn8view0 Import selector from recoil. Export const safeSelector equals selector with key transactions/defaultSelector, get retrieveTransactions, cachePolicy_UNSTABLE with eviction most-recent. Comment controlled eviction instead of keep-all.&lt;/p&gt;

&lt;p&gt;AI versus Human: Recoil output comparison table intentionally simplified. I prepared following table to quickly answer question "can I take Recoil code written with AI to prod same day". Recoil-specific risks like key stability, snapshot retain discipline, family scale, _UNSTABLE APIs affect most criteria. citeturn8view1turn6view2turn7view0turn10view0&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;AI Generated Recoil Code&lt;/th&gt;
&lt;th&gt;Human Generated Recoil Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correctness&lt;/td&gt;
&lt;td&gt;Mostly "works" but fragile in edge cases like retain, race, cache&lt;/td&gt;
&lt;td&gt;Validated with behavior-focused tests, modeling according to intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debuggability&lt;/td&gt;
&lt;td&gt;Snapshot/time-travel errors and "subscribe to all state" traps frequent&lt;/td&gt;
&lt;td&gt;Keeps debug tools in dev, snapshot lifetime and cost managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintainability&lt;/td&gt;
&lt;td&gt;Key standards, normalization, file organization tend weak&lt;/td&gt;
&lt;td&gt;Standards, key dictionary, reusable selector/atom patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package size&lt;/td&gt;
&lt;td&gt;Mostly similar but AI can suggest "hallucinated package" (supply chain risk)&lt;/td&gt;
&lt;td&gt;Package choice conscious, dependency policy and security scanning exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime safety&lt;/td&gt;
&lt;td&gt;Race condition, memory leak patterns, _UNSTABLE API lock risk&lt;/td&gt;
&lt;td&gt;Scale and maintenance plan considered, "archived repo" reality accounted for&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>recoil</category>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Zustand</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Wed, 18 Mar 2026 00:37:39 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-zustand-281f</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-zustand-281f</guid>
      <description>&lt;p&gt;This report analyzes in depth technical and conceptual limitations AI assistants can encounter when using Zustand. We addressed the topic under five main headings: store architecture and normalization, complex async flows and side effects, middleware and listener ordering nuances, performance with subscriptions and memoization traps, and migration/update and ecosystem compatibility. Each section includes technical explanations, real error modes where AI gets stuck, GitHub case studies, and code examples.&lt;/p&gt;

&lt;p&gt;For instance under Store Architecture heading, how Zustand's persist middleware works and functions added to state cannot be automatically saved were emphasized. AI generally includes a function like login in persist without considering this serialization limitation, and application cannot find this function when reloaded getting error. In Middleware section, noted that order of middlewares like persist, querystring, and immer affects behavior. AI assistants mix up middleware order leading to data merging or priority problems. In comparative table, Zustand code generated by AI and code written by human hand get evaluated in terms of correctness, accessibility, maintainability, package size, and runtime safety. Flow diagram models developer and AI collaboration, showing decision points to check at each stage. From outputs, clearly understood that human supervision is critical in situations AI can easily skip.&lt;/p&gt;

&lt;p&gt;For this study, first Zustand's official documentation and README were examined. Then GitHub issues and discussion topics about middleware order, persist problems, subscriptions and related blog posts were evaluated. For instance dev.to article about state persist addressed persistent store's rehydration problems. Literature and case studies examining AI code assistant errors like LinearB research were also utilized. In light of obtained data, five topics were determined and technical details specific to each, code examples, real case study examples, and solution recommendations were developed. Throughout report relevant information was supported with sources.&lt;/p&gt;

&lt;p&gt;In Zustand, store structure is flexible but correct modeling of state is important. Though no official normalization tool like in RTK, certain parts of state can be saved to localStorage with persist middleware. For example import create from zustand and persist from zustand/middleware. Const useStore equals create with persist and set function containing user object with name and email, token null, login async function with user and token parameters setting user and token, with name auth-store.&lt;/p&gt;

&lt;p&gt;In this code, persist middleware's purpose is making specified slice persistent. But AI assistants can manage side effects in this structure incorrectly. For instance while primitive types like user and token serialize well, login function cannot be stored in JSON. Developer had observed login function disappearing from state when page refreshed. This is expected situation because JSON.stringify only supports primitive types. AI code generally cannot notice this serialization limit, adds login function inside persist expecting it works, but in real life function doesn't get preserved.&lt;/p&gt;

&lt;p&gt;AI failure modes: AI thinks store content as simple data trying to serialize complex objects or functions. In this case code AI created saves items that cannot be converted to JSON and cannot access later. For instance a date like new Date or class instance doesn't give value you expect after persist. Additionally using non-normalized large array or object structures also causes repetitive updates. AI generally keeps state organization flat and doesn't separate data dependencies like related objects.&lt;/p&gt;

&lt;p&gt;Developer strategies: keep store design simple and use values serializable to JSON. Avoid storing functions and class instances inside state. When using persist, use only pure data like primitive types and pure objects. Manually review store structure AI created. If carries function or complex object, extract these outside state. Manage events inside action instead of functions like login. Manually fix normalization AI skipped, divide state into parts when needed or use separate slices. For instance keeping user information in separate slice and persisting it prevents confusion of functions. Ultimately state model should be planned, AI outputs should be subjected to manual normalization when needed.&lt;/p&gt;

&lt;p&gt;Zustand supports async/await directly, doesn't need middleware like redux-thunk. For instance fetchTodos function can be defined directly inside store. However in complex side effect scenarios, AI assistant limitations emerge. Especially in situations requiring multiple async steps or error management, AI can lean toward simple solutions. For instance in scenario refreshing user's token, AI focuses on just adding async to login function and returning expected value. In real life if refresh fails, need to catch error. AI code generally doesn't put try/catch structure correctly. When error happens, application crashes due to uncaught promise. Additionally AI generally does step-by-step async tasks with .then/.catch chain instead of await and forgets returning error. This produces code not suitable for async thunk based state management.&lt;/p&gt;

&lt;p&gt;Real example showing user authentication process. AI can produce code like this. Const useAuthStore equals create with set function containing loading false, error null, authenticate async function with credentials parameter. Set loading true. Try with const response equals await api.login credentials, set loading false and user response.user. Catch err with set loading false and error err.message. If AI forgot a problem like set loading false and error err.message, loading can stay true forever or error contains wrong value. In real QA scenario, AI output generally doesn't contain such finally blocks.&lt;/p&gt;

&lt;p&gt;Developer strategies: catch errors correctly in every async function. Using try/catch/finally blocks, close loading state in every situation. In mandatory cases additionally trigger with useEffect or onMount. Manually add try/catch or set calls missing in async function from AI. For complex scenarios like unusual side effects and rollbacks, consider using additional helpers from zustand/middleware like subscribe-with-selector. Additionally combination with solutions like React-Query instead of global API calls reduces AI's error-making area.&lt;/p&gt;

&lt;p&gt;Zustand combines middleware chain with compose method. For example const useSearchStore equals create with persist wrapping querystring wrapping immer with set function, query params config, and name search. In above code, persist, querystring, immer get applied in order. When order changes, behavior also changes. Developer had observed querystring arrow persist arrow immer order gives different results compared to persist arrow querystring arrow immer order. But AI assistant generally neglects putting middlewares in correct order or does reverse. This leads to errors in state merging and priority ordering. For instance if persist comes first, localStorage gets used first, whereas if querystring comes first, URL parameters gain priority. Wrong ordering can cause unexpected default behaviors.&lt;/p&gt;

&lt;p&gt;For listeners also ordering is important. If a subscribe or addListener function will be called from inside middleware or React hook, structure needs preserving. Otherwise can have repetitive calls or memory leaks. For instance if you create subscriber outside store and don't clean, new listener gets added at every page redirect and memory increases.&lt;/p&gt;

&lt;p&gt;Developer strategies: check order of all middlewares used. Adjust recommended combination like persist querystring immer by looking at examples in documentation. In store definitions from AI, pay attention to middleware callback function. If static array written, fix it. In subscriptions, use subscribe inside useEffect and do unsubscribe on component unmount. When you see missing unsubscribe in AI codes, definitely add it. Additionally when working with external triggers like URL parameters or storage, check usage examples in each middleware's docs. Briefly side-effect ordering should be manually validated.&lt;/p&gt;

&lt;p&gt;Zustand gives useStore hook for selectors where you take desired state slice and subscribe component. However AI assistants generally overlook memoization side. For instance const count equals useStore with state arrow state.counter.value, when used like this, component re-renders only when counter.value changes. But AI sometimes can write like this. Const state equals useStore with state arrow state, or const count equals useStore with state arrow state.counter. This usage causes component to render even when another field in state changes. Thus application slows down unnecessarily. For performance, zustand/shallow or customized equality functions can be used.&lt;/p&gt;

&lt;p&gt;When using subscriptions or store.subscribe, also should be careful. For instance a listener triggers at every state update. Especially in large and frequently changing states, callback running at every change slows application. Code written by AI generally contains anonymous functions listening to every change. AI assistant might not specify selected state pieces. For performance, using subscribe with selector and callback is recommended. Also in useEffect plus subscribe usage inside React components, if unsubscribe not called, memory problems happen.&lt;/p&gt;

&lt;p&gt;Developer strategies: when using useStore, always select only state fields you need. If possible apply shallow or custom equality function. Review subscriber codes from AI. If anonymous function listening to all state exists, customize this. For example subscribe correctly with const unsub equals useStore.subscribe with state arrow state.token and token arrow console.log Token changed token. Cleanup with unsub. This will trigger only when token changes. Additionally for large state, consider making immutable updates with libraries like immer. But sometimes in AI code, wrong set usage catches eye like set nested with spread. When nesting deepens, prefer using produce with immer. Thus you use clean approach instead of complex update codes AI created.&lt;/p&gt;

&lt;p&gt;The following table presents general comparison in Zustand context between AI generation and human generation. Correctness medium containing function serialization, subscriber cleanup, and middleware order errors versus High with persist/rehydration and subscriptions managed correctly and tests done. Accessibility weak with code comments and structure explanations generally missing and state changes can be overlooked versus Good using explanatory slice and selector names and clear error handling.&lt;/p&gt;

&lt;p&gt;Maintainability low with short auto-generated codes mostly undocumented in inline usages versus High with comprehensive documentation and examples and well-designed hooks. Package Size low because Zustand is small library so no noticeable difference, Zustand around 1.8KB versus Low again lightweight but extra middlewares can be manually added. Runtime Safety medium with runtime problems like subscription mismanagement and function persistence errors possible versus High with antipatterns tested and warnings tracked in React build process.&lt;/p&gt;

&lt;p&gt;The mermaid flowchart shows workflow between developer and AI. In first step, checked whether functions exist in store content. If functions exist, moved to external sources needing serialization. Then middleware ordering and async functions inspected. Finally appropriateness of subscriptions and performance strategies evaluated. After each check when found incomplete, necessary corrections made. This process aims to detect and address error-prone points in Zustand code AI created.&lt;/p&gt;

&lt;p&gt;Flowchart shows: Requirements including store model, middleware/listing, async needs. Get Zustand code from AI. Decision whether functions in state. If yes extract these functions outside serialization and return. If no decision whether middleware order correct. If incomplete return. If complete decision whether async operations checked. If incomplete return. If complete decision whether subscribe and performance adjusted. If incomplete return. If complete code review and test approval.&lt;/p&gt;

</description>
      <category>zustand</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Redux Toolkit</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Sun, 15 Mar 2026 00:16:17 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-redux-toolkit-43pn</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-redux-toolkit-43pn</guid>
      <description>&lt;p&gt;This report examines important technical and conceptual challenges AI-assisted tools can encounter when using Redux Toolkit or RTK. Beyond conveniences Redux Toolkit offers, situations were addressed where AI assistants can fail in five critical topics: store architecture and normalization, complex async flows and side effects, middleware ordering and composition, performance and memoization traps, and update/upgrade and ecosystem compatibility. Each section provided technical details, concrete scenarios where AI can make errors, and real-world examples.&lt;/p&gt;

&lt;p&gt;For instance in Store Architecture, importance of data normalization using createEntityAdapter in RTK was explained. AI generally leads to chaos by putting related data in wrong model. In Async Flows section, example given how not handling errors correctly with createAsyncThunk causes problems. In Middleware section, emphasized that middleware arrangement inside configureStore must be given as callback. If AI gives array directly and doesn't add default middleware, important functions stay missing. Additionally in table we compared AI outputs to human outputs in criteria like correctness, debugging, maintainability, package size, and runtime safety. Because AI code can skip patterns RTK recommends, emphasizes necessity of human supervision and code review. In conclusion, importance of human intervention clearly emerged in Redux Toolkit projects as well.&lt;/p&gt;

&lt;p&gt;For this study, first Redux Toolkit's official documents and release notes were scanned. Attention paid to topics like store configuration, asyncThunk usage, middleware configuration, and code migrations. GitHub issues about memory leak and migration and StackOverflow questions were examined. Developer discussions about RTK memory management like immer-sourced leaks were evaluated. Research about AI code assistant limitations like LinearB code analysis was also reviewed. Obtained findings were explained with examples in five topics. Each section contains technical explanations, faulty AI scenarios, code snippets, and development strategies. Citations to sources are directly linked with relevant topics.&lt;/p&gt;

&lt;p&gt;Redux Toolkit facilitates structuring store with entity-based normalization. In RTK, normalizing data with createEntityAdapter is common approach. For instance for blog application, posts and comments states can be kept like this. Import createSlice and createEntityAdapter from redux toolkit. Post entity adapter with const postsAdapter equals createEntityAdapter and commentsAdapter equals createEntityAdapter. Example normalized initial state with posts as postsAdapter.getInitialState, comments as commentsAdapter.getInitialState, and users as empty array. Slice using state update with const postsSlice equals createSlice with name posts, initialState posts, and reducers containing postAdded as postsAdapter.addOne and postsUpdated as postsAdapter.upsertMany.&lt;/p&gt;

&lt;p&gt;createEntityAdapter facilitates performing update in single center by storing each item in only one place. AI tools generally skip this normalization. For instance can keep related post and user objects in different places both in posts and users slice, making update synchronization difficult. AI writing plain state.posts equals array with spread without knowing RTK standards like entityState breaks normalization. As result reducers become complicated, to update a record requires operating on many slices.&lt;/p&gt;

&lt;p&gt;Developer strategies: normalize state using createEntityAdapter. Check AI output ensuring each data type gets stored using singular IDs. If needed examine RTK's example projects. Restructure AI's plain array or nested object suggestions with adapter. Do immutable updates with adapter's methods like addOne and setAll. Normalization especially in related data reduces bug possibility. If AI violates normalization rules, reorganize files ensuring compliance.&lt;/p&gt;

&lt;p&gt;Redux Toolkit manages async actions with tools like createAsyncThunk. However AI helpers can use this structure incorrectly. For instance error handling topic is critical. If you don't catch error inside createAsyncThunk, promise automatically gets rejected triggering rejected action. But AI generally returns all errors with return and marks as fulfilled. In such situation application goes to success state instead of error. In StackOverflow question, developer using try/catch inside createAsyncThunk observed returning fulfilled when network cut. As solution in response, emphasized correct to directly await Axios call leaving without catching error. Another method is using rejectWithValue.&lt;/p&gt;

&lt;p&gt;Export const fetchData equals createAsyncThunk with data/fetch and async function with id and rejectWithValue parameters. Try with const response equals await api.get id, return response.data. Catch err with return rejectWithValue err.message as part AI skips. AI code mostly doesn't think to use rejectWithValue and writes return err after try/catch, detecting error as fulfilled. Thus UI can turn to wrong data instead of error.&lt;/p&gt;

&lt;p&gt;Regarding conditional operations and listener, tools like createListenerMiddleware or RTK Query recommended for more complex flows. AI most times tries solving problem with manual setTimeout or Promise chain. For instance in application needing renewal when user's token expires, listening to userLogin event inside createListenerMiddleware and renewing token is more robust instead of manual dispatch logout. AI might not think best in such effect-heavy scenarios.&lt;/p&gt;

&lt;p&gt;Developer strategies: use RTK's tools correctly for async operations. Review async thunk codes from AI, errors should be handled with rejectWithValue instead of directly returning. Also apply advanced methods like createListenerMiddleware or onQueryStarted with RTK Query. Provide control with methods RTK offers instead of manual promise. Briefly if you see missing error catching or wrong async configurations in AI codes, make correction with examples in RTK documentation.&lt;br&gt;
In Redux Toolkit, middleware chain given with configureStore determines arrangement. As of RTK 2.0, middleware must be defined in callback function. If you give direct array, default middlewares like thunk and serial check don't get added. For instance middleware array with myMiddleware definition doesn't add default thunk leading to unexpected deficiencies. According to RTK upgrade guide, middleware should only be callback, otherwise thunk and debug middlewares don't activate.&lt;/p&gt;

&lt;p&gt;AI assistants miss this detail. AI code typically writes like this. Const store equals configureStore with reducer rootReducer and middleware array with loggerMiddleware showing wrong here because default middlewares not added. In this example, if AI gave middleware directly as array, means no thunk RTK recommended. Correct usage should be like this. Const store equals configureStore with reducer rootReducer and middleware as function getDefaultMiddleware arrow getDefaultMiddleware().concat loggerMiddleware showing checkmark default middleware also added.&lt;/p&gt;

&lt;p&gt;Additionally RTK's control middlewares like serializableCheck or immutableCheck get provided automatically. These staying deactivated in AI code leads to runtime errors. Developer strategies: ensure configureStore.middleware usage done correctly by AI. If AI suggests array, convert it to callback format combining with getDefaultMiddleware. Check middleware count AI added, unnecessary plugins affect application performance. createListenerMiddleware or RTK Query providers generally should be taken to bottom of list. Since default middleware usage already emphasized in RTK documentation, always validate thunk is added.&lt;br&gt;
RTK provides selector and memoization tools but AI generally skips these. When using useSelector, if complex calculations exist, memoization should be applied with createSelector. For instance when filtering product list, simple selector can be written like this. Export const selectAvailableProducts equals state arrow state.products.filter with p arrow p.available. This code applies filter at every state change. Whereas can be memoized using Reselect like this.&lt;/p&gt;

&lt;p&gt;Import createSelector from redux toolkit. Export const selectProducts equals state arrow state.products. Export const selectAvailableProducts equals createSelector with selectProducts and products arrow products.filter with p arrow p.available. Code written with AI assistance mostly doesn't contain this optimization. Calculating at every render in large lists leads to performance problem. RTK's tools like createEntityAdapter provide fast CRUD on similar data. AI sometimes manages data with raw array operations doing unnecessary copying. Other problem in AI code is adding unnecessary code to build, pulling entire package from libraries like lodash increases bundle size similar to code binding.&lt;/p&gt;

&lt;p&gt;Developer strategies: using memoization in every createSelector or slice should be mandatory. Wrap operations like filter or map in AI code with useMemo if need to calculate once and store. RTK using immer in immutable updates optimizes change by default, therefore pay attention to deep copies. If possible paginate taking large states in components. Against extra dependencies AI suggested, import only really needed lodash methods like import get from lodash/get format. Measure performance with tools like Redux DevTools Profiler discovering bottlenecks.&lt;/p&gt;

&lt;p&gt;The following table presents general comparison in RTK context between AI generation and human generation. Correctness low with wrong normalization, faulty async thunk management, missing default middleware usage versus High with configuration compliant with RTK recommendations, necessary middleware and error handling exist. Debugging Ease medium with actions and state structure mixed, RTK DevTools provides most info but errors stay hidden versus High with naming of createSlice and thunks, debug tools like RTK Query used.&lt;br&gt;
Maintainability low with AI outputs generally containing repetitive hard-to-understand codes versus High with little code using createSlice and createAsyncThunk, clear structure, explicit comments. Package Size medium with possibility of unnecessary library loading, code block conflict if thunk missing versus Low with tree shaking effective, minimal dependencies with RTK recommendations. Runtime Safety medium with wrong data type management like serialize errors, memory leak risk with immer versus High with immutable updates, serializable state guarantee, strict TS type usage.&lt;/p&gt;

&lt;p&gt;The mermaid flowchart shows developer first reviews Redux Toolkit code obtained from AI for normalization, async thunk usage, and middleware. If deficiency exists, corrects and repeats process. Thus AI output passes through human control. For instance in RTK upgrades, need to convert object form to builder form for extraReducers. Every step AI skipped should be manually completed by developer.&lt;br&gt;
Flowchart shows: Requirements including RTK installation, slice structure, async requirements. Get Redux Toolkit code from AI. Decision whether state normalization done. If no normalize with createEntityAdapter and return. If yes decision whether async thunks and error handling appropriate. If incomplete return. If complete decision whether middleware configuration correct. If incomplete return. If complete decision whether memoization and performance optimization done. If incomplete return. If complete code review and live test approval&lt;/p&gt;

</description>
      <category>redux</category>
      <category>tooling</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Redux</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Fri, 13 Mar 2026 21:51:34 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-redux-3i37</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-redux-3i37</guid>
      <description>&lt;p&gt;This report examines technical and conceptual limitations AI-assisted tools can encounter when using Redux. Deep analysis will be presented under five topics in context of Redux's unique architecture and ecosystem: store architecture and normalization, complex async flows and side effects, middleware ordering and composition, performance and memoization traps, and update/migration and ecosystem compatibility. Each section includes technical details, concrete error modes explaining why AI fails, real-world examples from GitHub issues and case studies, and code snippets.&lt;/p&gt;

&lt;p&gt;For instance in Normalization section, data normalization format Redux recommends is explained and keeping each data type as separate table gets emphasized. AI assistants can make errors in normalizing complex related data deeply in one go, locking update operations. In Middleware section, emphasized that ordering written inside applyMiddleware directly determines operation. According to StackOverflow answer, defined middleware gets processed in order, changing ordering later isn't possible. AI code sometimes depends on flow inside function instead of middleware order, creating unexpected behaviors. Table compares AI and human outputs with criteria like correctness, debugging, maintainability, package size, and runtime safety. Finally mermaid diagram summarizing developer and AI collaboration shows which decisions to make at each stage. Study results clearly reveal human supervision still needed in Redux projects due to AI limitations.&lt;/p&gt;

&lt;p&gt;For this report, first official Redux documents and Redux Toolkit documentation with release notes were compiled. Then StackOverflow questions, GitHub issues, and blog posts were scanned, for example normalization, middleware flows, and performance issues. Migration stories about async flows and side effect management like thunk versus saga were examined. Memory leak and performance-focused case studies like weakMapMemoize problems in a Redux Toolkit version were addressed. Literature about AI code assistant limitations like LinearB code quality research was also reviewed. Obtained findings were explained with technical clarifications, code examples, and solution recommendations under five topics. Each section cited relevant sources and AI error modes were supported with real examples.&lt;/p&gt;

&lt;p&gt;Redux store generally gets structured with data normalization method. In official Redux guide, normalized structure is recommended for complex related data. That is, separate table gets created for each data type and items get stored with their IDs. For instance blog posts and comments get kept in separate objects, relationships established only through IDs. Example showing normalized Redux store structure. Const initialState with posts object containing byId with post1 containing id, title, commentIds array, and allIds array, comments object with byId containing c1 with id and text, and allIds array, users object similar.&lt;/p&gt;

&lt;p&gt;Thanks to normalization, each item gets defined in only one place, thus update operations get done in single center. AI tools can skip this concept, might try storing complex nested data structure inside single-piece state. For instance if an AI tries keeping both posts and comments list inside state.posts, data updates get processed in two different places increasing error risk. Additionally in non-normalized structure, deep data updates require writing code nested and increase error-making possibility.&lt;/p&gt;

&lt;p&gt;AI failure modes: AI-assisted code can embed related data in same object and skip normalization. In this case update logic inside reducer gets complicated. For instance to update author name in blog post, both author object inside post and author object in users list should be updated. AI generally tries handling this with single-line code getting wrong result. Additionally might prefer making customized normalization without using Redux Toolkit tools like createEntityAdapter. Ultimately can lead to inconsistencies in state structure and unexpected re-renders.&lt;/p&gt;

&lt;p&gt;Developer strategies: use really normalized state structure. Adopt byId/allIds model shown in Redux documents. Definitely review state structure AI suggested. If nested repeating data exists, apply normalization. Benefit from examples coming with RTK's createEntityAdapter or createSlice. Update reducers become simpler in normalized structure. If AI neglected bringing normalization, manually fix store. Ultimately consistent and normalized state arrangement especially in complex data relationships reduces error possibility.&lt;/p&gt;

&lt;p&gt;Redux basically doesn't manage asynchronous workloads, for this work middleware or additional tools like thunk, saga, observable are needed. AI assistants might not combine these structures correctly. For instance when using Thunk, might mistakenly return async function itself instead of dispatch or can write wrong try/catch. Additionally for multiple async steps might completely skip solutions like saga or RTK Query trying to produce manual solution. Real-world example: developer had created pattern like dispatch asyncThunk then when using asyncThunk. AI lost control after dispatch by not managing this promise chain properly. Such errors led to not catching errors or unexpected state updates. On other hand in project using Saga, AI-sourced code used take instead of takeEvery, causing only first action to work and listening to stop afterward.&lt;/p&gt;

&lt;p&gt;AI failure modes: AI leans toward simple solutions in async workflows. For instance tries writing all business logic inside single Thunk for complex REST call, whereas generally dividing to side effects layer like saga or RTK Query is more correct. AI additionally can skip error catching by using .then instead of await in promise-based calls like axios. AI code falls short in reflecting errors occurring during side effects to Redux state, for instance can forget using rejectWithValue.&lt;/p&gt;

&lt;p&gt;Developer strategies: use tested methods for async flows. Proceed with createAsyncThunk, createListenerMiddleware, or saga templates in React-Redux application when possible. Definitely look at try/catch blocks in code from AI. When triggering an action, ensure you handle and dispatch both success and failure states. For advanced scenarios examine redux-saga or redux-observable approaches. Since AI won't do automatic configuration, manually integrate according to documentation examples. Carefully review Thunk or saga codes AI generated, validate async steps are complete.&lt;/p&gt;

&lt;p&gt;Redux middlewares are functions intercepting when actions get dispatched. Order is very clear. Middleware array defined as applyMiddleware m1, m2, m3 processes in exactly that order. That is if you wrote m1, m2, m3, m1 first then m2, when m3 comes real reducer runs. StackOverflow answer clearly expressed this: middleware pipeline exactly matches order you wrote to applyMiddleware and you cannot change after creating it. AI assistant generally misunderstands middleware flow. For instance wrong AI output puts protective middleware at end of list thus running after all actions processed, whereas this middleware should be placed at start to break actions like blocking unauthenticated ones. AI additionally can confuse relationships with compose or combineReducers. Common error is forgetting next call in async middleware. In this case no reducer runs and action stays halfway.&lt;/p&gt;

&lt;p&gt;Example showing filtering actions in middleware. Const logger equals store arrow next arrow action with console.log dispatching action, return next action with warning that without next action ends here. Above if next action not added, action chain stops here. This omission can be easily made when written by AI. Developer strategies: carefully plan middleware ordering. Review applyMiddleware order AI suggested, pay attention whether critical middlewares like auth check or error reporting are in right place. Ensure next action not missing in function definitions. If using multiple middlewares, design so they don't interact with each other. For instance ensure thunk or saga middlewares are definitely in bottom order so other middlewares can see async actions. Verbal test: check by asking Does action continue flowing without this middleware.&lt;/p&gt;

&lt;p&gt;Performance and re-render behavior of Redux applications relate largely to memoization and selective updates. AI assistants generally produce unnecessary recalculations. For instance if complex calculations exist inside useSelector and this selector not memoized, recalculation happens at every state change. AI sometimes forgets using createSelector. Whereas memoized selectors don't do calculation when called again with same inputs. Real example: when developer wrote selector to filter from products list, AI code did like this. Export const selectFilteredProducts equals state arrow return state.products.filter with p arrow p.visible and p.stock greater than 0. This code does filtering after every dispatch. Instead should have used memoized selector with reselect.&lt;/p&gt;

&lt;p&gt;Additionally instead of storing large lists in Redux state should be solved with other methods like pagination or off-memory DB. AI most times when updating large arrays kept in state keeps old objects even due to immer and memory usage increases. For instance when updating list of 100,000 items, approach like setState with spread causes both high package size and heap growth. If using RTK, consider doing normalization and memoized operations with createEntityAdapter.&lt;/p&gt;

&lt;p&gt;Developer strategies: make using reselect or RTK createSelector mandatory in selectors. In complex state changes use immutable methods like RTK's current function instead of shallow copy. When you see large constant lists in AI code, question whether these really need to be kept in state. Optimize React components with useMemo or useCallback. After each render look at console which components re-rendered. Detect bottlenecks using performance tools AI might have forgotten like Redux DevTools Performance tracing. Ultimately you can speed up application by manually removing performance traps that can emerge in AI code.&lt;/p&gt;

&lt;p&gt;The following table presents general comparison in Redux context between AI generation and human generation. Correctness low with wrong normalization, missing side-effect management, wrong middleware order versus High with state normalization, async flows, middleware order validated. Debugging Ease weak with action history DevTools meaningless and error origin point unclear versus Good with regular action type and state structure, understandable tracking with DevTools.&lt;/p&gt;

&lt;p&gt;Maintainability medium with much code repetition and lack of comment seen in AI code versus High using RTK and slice structure, clean code, good documentation. Package Size medium with unnecessary dependencies like all lodash packages possibly added versus Low with only needed libraries, tree shaking and bundle analyses done. Runtime Safety medium with API errors and memory leaks like circular references visible versus Good using immutable updates, appropriate memory management and error handling.&lt;/p&gt;

&lt;p&gt;The mermaid flowchart summarizes AI collaboration in Redux development process. First store architecture and normalization checked, if needed state gets organized. Then correctness of async tasks and middleware flow evaluated. Performance optimizations like memoized selectors addressed. After passing all stages, code tested and approved. If error found, returns to process. At each step if deficiency exists, intervention should be made to AI code.&lt;/p&gt;

</description>
      <category>redux</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Babel</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Thu, 12 Mar 2026 20:50:13 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-babel-3h5j</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-babel-3h5j</guid>
      <description>&lt;p&gt;This report addresses limitations emerging when using Babel with AI-assisted tools. Five main topics examine Babel's plugin and transform pipeline, AST-level transformations and semantic intent, source maps and debugging, performance and caching determinism, and compatibility with polyfill and runtime semantics. Each section provides technical explanations, frequent error modes AI falls into, and real-world examples. For instance in Transform Pipeline section, how critical plugin application order is gets emphasized. According to Babel documentation, plugins get applied in order they're written in configuration file, while presets work in reverse order. In a GitHub error, when import placement done incorrectly in automatic JSX transform with importPosition after, error occurred in Jest tests. Such surprises are details AI assistant cannot notice.&lt;/p&gt;

&lt;p&gt;In AST Transformations section, Babel's operation steps on AST and its limitations are explained. Babel parses a file, processes AST, then converts back to source code. During this process, for example Recast library's original attribute can be lost and differences appear in format information. Adding strict mode between codes is typical example of this. In Source Maps and Debugging section, explained that even AST traversal without transformation can break source map. AI assistants generally skip source maps, leaving errors pointing to transformed code instead of original file in browser console. Ultimately, code generation without detailed review and testing with Babel leads to incorrect or hard-to-debug results.&lt;/p&gt;

&lt;p&gt;Table compares Babel configurations generated by AI with configurations written by human hand in terms of correctness, debugging ease, maintainability, deterministic build, and runtime compatibility. This analysis bringing curious points to forefront shows human supervision is essential in Babel projects as well. For this study, primarily official Babel documents and changelogs were examined. Notes about plugin API and compilation process from StackOverflow and GitHub issues were collected. For real-world problems, relevant GitHub issues and community blogs were scanned, for example React JSX plugin error and Recast integration. For source maps and error management, MDN and Babel guides were reviewed. AI code assistant analyses were also included. Obtained data was processed with examples in five distinct topics with Babel's own terminology. In each section technical explanations, concrete error modes, code examples, and remediation strategies were given. Report flow was arranged to facilitate developer reading.&lt;/p&gt;

&lt;p&gt;Babel performs code transformations with plugins and presets. Important subtle point is application order. Plugins run in order written in configuration, while presets get applied in reverse order. For example in .babelrc, with plugins array containing transform-decorators-legacy and transform-class-properties, and presets array with babel/preset-env and babel/preset-react, in this configuration first transform-decorators-legacy then transform-class-properties plugins get applied. For presets, React preset then Env preset activates, preset order gets reversed. Wrong ordering can cause code to be transformed unexpectedly.&lt;/p&gt;

&lt;p&gt;For example in error reported on GitHub, babel/preset-react automatic JSX transform with runtime automatic was placing import lines in wrong position. In this case jsx-runtime imports were added to end of other code and jsxRuntime stayed undefined in Jest tests. Simply if not set as importPosition before, AI code output can lead to timing error. AI failure modes: AI assistants generally skip or misunderstand plugin order. Doesn't think about a plugin's interaction with previous transformation. For instance if order of usages like class properties and decorators is wrong, solution path breaks. Nuances like using preset mixture instead of plugin can be overlooked in AI code. Additionally compile-time options like only/ignore filters and env.targets can be skipped. According to official Babel configuration examples, plugin orders and presets compatibility should be manually validated.&lt;/p&gt;

&lt;p&gt;Developer strategies: pay special attention to ordering. When reviewing .babelrc or babel.config.js outputs from AI, ensure plugin array is in correct order. If custom plugins used like parserOpts.plugins, these should be configured in right place. If you see a transformation giving error, test by swapping plugins. Like in above JSX example, temporary solution was found by changing importPosition setting. Ultimately manual check is mandatory that plugins are running in ordering that will produce effect you want.&lt;/p&gt;

&lt;p&gt;Babel first makes source code into AST or Abstract Syntax Tree, then manipulates this AST and converts back to code. AST-level transformations frequently require expertise. For instance a syntax plugin only prepares parser for new syntax, while transform plugin converts AST nodes to target language. This three-stage process looks like this: parsing source code to AST, modification/transformation on AST, and printing AST back to source code. AI failure modes: AI code generators can misinterpret AST clarifications. For instance to transform a class property, generally both syntax and transform plugins are needed. Inspired by above example, if only transform plugin added and syntax plugin forgotten, Babel cannot parse unconventional syntax.&lt;/p&gt;

&lt;p&gt;According to SO answer, if code contains new syntax like at.foo semicolon, for parser to understand this, syntax plugin must be used first, otherwise error gets received in AST creation. AI tools sometimes miss this difference and focus only on transformation part. Another example is difficulty of transforming without breaking code's meaning. In error report on GitHub, original information Recast library added to AST nodes got lost during Babel transformation. As result, as seen in images, code's format changed with use strict added and line endings shifted. This is side effect difficult for AI to notice. Transformer code actually works correctly but format in original source gets lost.&lt;/p&gt;

&lt;p&gt;Another error mode is transformers overlapping or creating opposite effect. For instance while transform-classes plugin changes constructor function, if another plugin makes different change to same node, order matters. In AI-assisted code, situations where two plugins conflict with each other can be overlooked. Developer strategies: ensure plugins are used appropriately for their purpose. Syntax plugin automatically added by AI should be checked. If code still cannot be parsed, syntax plugin or preset might be missing. In plugins changing AST, sometimes directives like path.skip or path.remove being in wrong place inside visitor can break transformation. When working with tools like Recast, when functions like babel.transformFromAst are used, pay attention that original information doesn't get thrown away. If difference occurs in format after code transformed like added use strict or line shifts, consider checking these transformation settings or generatorOpts parameters. Ultimately AI codes definitely require logical review at AST level.&lt;/p&gt;

&lt;p&gt;When using Babel, source maps and error tracking are very critical. When Babel transforms code, generally adds or removes new lines, causing error when looking at original file. In real example, even though no change made on AST, transformFromAst function broke source map because processed spaces differently. For instance formatting differences in return writing gets marked by compiler skipping point. As mentioned in StackOverflow answer, AST transformations break source map, therefore producing new maps becomes mandatory. In AI-assisted code, source map generally gets overlooked. Developer might notice code appearing error-free explicitly references different lines in dev tool. Error messages come according to code Babel generated, not original code. For instance if Unexpected token error given, in transformation created with AI templates might have no relation to actual line.&lt;/p&gt;

&lt;p&gt;Regarding debugging and stack trace, Babel's error reports are sometimes misleading. For instance in errors like TypeError undefined during a test, AI code generally shows transformed names. Tools like babel-code-frame might be needed to reach code's actual source. Additionally during babel/plugin-transform-runtime or babel/polyfill usage, global variable conflicts can emerge. AI can easily neglect these. Developer strategies: create new source map after every Babel transformation. As mentioned in above SO response, update maps using transformFromAstSync with ast, code, and options including sourceMaps true and inputSourceMap oldMap. By developer's hand, should be validated that lines error messages show match original source lines. When working with Babel CLI or Webpack plugins, devtool source-map settings should be configured correctly. When you get error under code, check AST output examining spans and location information. Additionally if you see Babel helper function names in error stack traces, babel-code-frame or Chrome devtools Decompiler features should be used. Ultimately Babel configuration from AI should definitely be gone over to allow tracing and mapping error source after every transformation.&lt;/p&gt;

&lt;p&gt;Babel affects compilation times and package sizes especially in large projects. Babel configurations generated by AI generally contain unnecessary plugins or complex plugin chains. For instance should be tested whether plugin doing every transformation gets applied. Using many small plugins might not provide significant advantage. Performance-wise, build caching like babel/plugin-transform-runtime and babelHelpers should be configured correctly. Additionally Babel might not behave deterministically even when input source doesn't change. Some plugins can assign random IDs. Regarding caching, when using Webpack/Babel loader, options like cacheDirectory being open seriously shortens compilation time. AI code can generally forget this step. Real case: in large monorepo during Babel 8 update, all packages' compilation took seconds when cache was open versus reaching minutes when closed.&lt;/p&gt;

&lt;p&gt;Regarding determinism, some plugins can give different results at different compilation times. For instance generating unique helper function names at each run with babel/plugin-transform-runtime. AI assistant cannot see reason for this. For build to stay deterministic, Babel's modes like assumeMutableTemplateObject or loose might need to be fixed. In current Babel options there's no parameter like deterministicUUIDs, therefore additional checks should be put in AI-generated configuration to get same result.&lt;/p&gt;

&lt;p&gt;Developer strategies: reduce plugin count for Babel performance, do only necessary transformations. In Webpack enable loader cache with cacheDirectory true and cacheCompression false. In plugins AI recommended like transform-runtime, test options like helpers true or regenerator false. Repeat builds several times comparing results. If difference exists, review configuration. Before taking ready solutions AI advised to production, do performance tests in your own application.&lt;/p&gt;

&lt;p&gt;Babel is used to convert language's new features to old environments. But AI assistants can fall short on this topic. For instance during babel/preset-env usage, if target browser list or targets not determined correctly, some polyfills don't get added or get added unnecessarily. In real migration story, when a team mistakenly used entry mode instead of useBuiltIns usage with preset-env, bundle size grew astronomically, AI recommendations couldn't see this. Regarding polyfill mismatches, core-js and regenerator-runtime configuration generally gets skipped in AI tools. In a bug report, AI code transformed code supporting async/await and didn't add needed runtime, giving error in browser.&lt;/p&gt;

&lt;p&gt;Regarding runtime differences, some semantic changes happened between Babel 7 and 8. For instance in topics like new pipeline operator or private fields, runtime behaviors changed. AI's automatic update can ignore these differences. Additionally in special modes like JSX Runtime, runtime like React jsx-dev-runtime should be manually configured. Regarding security and compatibility, when using babel/plugin-transform-runtime, selecting correct corejs version is important to prevent global pollution. AI doesn't test wrong corejs version compatibility.&lt;/p&gt;

&lt;p&gt;Developer strategies: customize babel/preset-env configuration appropriately for your project. Give targets list explicitly and validate which polyfills get added, see browserslist structures. Manually check options like regenerator true/false and corejs AI added in code. In migration projects, create small test files to validate runtime semantics like private class field and optional chaining. In Babel version upgrades, examine changelog. If AI code doesn't notice feature requiring polyfill, manual addition should be made. Ultimately configure Babel not just automatically but manually according to your needs.&lt;/p&gt;

&lt;p&gt;The following table presents general comparison in Babel context between AI generation and human generation. Correctness low with wrong plugin ordering and missing parser plugin causing incorrect transformations versus High with AST changes and semantic transformations checked. Debugging Ease weak with broken source maps and unclear stack trace, errors not matching original code versus High with code and original source matching and clear error messages received.&lt;/p&gt;

&lt;p&gt;Maintainability medium with complex configurations and lack of comments, AI code generally doesn't contain lines/comments versus High with clear configuration and comments, well documented. Build Deterministic low with different results in concurrent builds, some plugins are non-deterministic versus High with caching active and consistent output provided with same config. Runtime Compatibility medium with missing polyfills and wrong runtime settings like jsx-runtime common versus High with polyfill and runtime requirements like core-js and runtime properly set.&lt;/p&gt;

&lt;p&gt;The mermaid flowchart models a developer-AI collaboration. At start, .babelrc or babel.config.js configuration obtained from AI gets checked for ordering and plugin/preset usability. If needed ordering gets adjusted. Next step validates transformations to be done on AST like React JSX and ESNext syntax. Then source maps and debug methods examined, map creation process AI skipped gets added. Performance settings and caching mechanisms like Babel cache and transform-runtime settings get tested. At each step if problem exists correction made, at very end process completed with manual test and code review. Through this loop, Babel code AI generated gets cleaned from errors.&lt;/p&gt;

</description>
      <category>babel</category>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>5 Things AI Can't Do, Even in Svelte.Js</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Thu, 12 Mar 2026 01:08:56 +0000</pubDate>
      <link>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-sveltejs-277b</link>
      <guid>https://future.forem.com/devunionx/5-things-ai-cant-do-even-in-sveltejs-277b</guid>
      <description>&lt;p&gt;This report examines in depth the limitations in Svelte.js development process with AI-assisted tools. Considering Svelte's compiler-based architecture, technical and conceptual challenges were analyzed in five topics: component semantics and composition, reactivity model and complex state flows, compile-time versus runtime behavior and edge cases, accessibility and ARIA integration, and performance with build toolchain and scale compatibility. In each topic Svelte-specific rules were explained and supported with concrete error examples AI makes and case studies.&lt;/p&gt;

&lt;p&gt;For instance in Reactivity Model topic, Svelte's reactivity based on variable assignments gets emphasized. When you update values inside object or array, for compiler to detect this you need to make assignment to variable itself. If this gets neglected in AI-assisted code, updates don't reflect to DOM. Additionally limitations arising from Svelte's compile-first design were examined. For instance html tag might not do correct hydration after SSR, or reactive classes like MediaQuery don't give correct result during SSR because no browser measurement exists. In each section, code examples and failure modes were addressed and solution paths discussed. For example to prevent async errors, using onMount and tick, cleanup with effect in code snippets shown.&lt;/p&gt;

&lt;p&gt;Table at end of writing compares Svelte code generated by AI with code written by human in criteria like correctness, accessibility, maintainability, package size, and corporate compliance. Finally a mermaid diagram showing developer plus AI workflow summarizes process steps. Findings clearly reveal that human supervision and experience remain indispensable in Svelte projects. For this study, primarily official Svelte documents and release notes were examined. Reactivity, compile-time features, and performance recommendations were scanned in detail in Svelte documentation. Then feedback from Svelte developer community including GitHub issues, blog posts, StackOverflow questions was evaluated. For accessibility standards, MDN and W3C/WCAG sources were reviewed. Literature and case studies analyzing errors in code AI code assistants wrote were also compiled.&lt;/p&gt;

&lt;p&gt;Obtained data was processed in depth in five main topics specific to Svelte. Under each topic, technical explanations, concrete error scenarios, example codes, and solution strategies were presented. Each section of report was supported with results using relevant and priority sources. Svelte components require tight integration with HTML. Interaction between components is provided with props and createEventDispatcher. Semantically, developer should always prefer valid HTML tags and avoid unnecessary wrapping. For instance when creating list item with li tag, this must definitely be inside ul or ol. Though not an error from Svelte itself, AI sometimes can make similar semantic incompatibilities.&lt;/p&gt;

&lt;p&gt;Another situation is bind:this usage. Svelte offers bind:this directive to get reference to DOM nodes. In following example, an input element gets bound to nameEl variable getting reference and after form submission gets pulled back to focus with focus. Script. Let name equals empty string. Let nameEl for DOM element reference. Function addTodo. Add new task operation. Name equals empty string. nameEl.focus for giving focus back to input. Script. Input bind:value equals name bind:this equals nameEl id equals todo-input. Button on:click equals addTodo disabled equals not name showing Add.&lt;/p&gt;

&lt;p&gt;Code generated by AI might skip this kind of bind:this usage leading to focus management problems. Also correct positioning of component contents with Svelte's slot mechanism is important. For instance modal component content should be placed with slot. AI sometimes might suggest transition with methods like direct innerHTML, which can be problematic in terms of accessibility and maintainability.&lt;/p&gt;

&lt;p&gt;Developer strategies: semantic HTML usage in AI code should always be reviewed. For instance instead of adding role equals button to elements that aren't button, use real button when possible. Props and event dispatch structures should be defined correctly. Missing createEventDispatcher usage in AI code should be carefully checked. If submission preventDefault or custom directive usage needed, should be manually added. When operations toward focus requiring bind:this usage are seen in code, should be manually added if AI skipped. Ultimately component composition should pass through human supervision.&lt;/p&gt;

&lt;p&gt;Svelte offers compiler-based reactivity system. State changes typically get tracked with assignment equals or dollar colon reactive declarations. Important rule: for Svelte to understand a variable updated, variable name needs to be on left side of assignment. For instance in reference types like arrays and objects when element update done, Svelte might not detect this. In following MDN example, completed field of items inside todos array gets updated but Svelte doesn't notice. Const checkAllTodos equals with completed parameter. Todos.forEach with t arrow t.completed equals completed. Todos equals todos notifying Svelte of change.&lt;/p&gt;

&lt;p&gt;In this code, though each item of array updated, Svelte doesn't observe this update because array itself didn't change. As solution, reassignment done like todos equals todos, this way compiler understands variable got modified. Alternatively updating each item from array's index or assigning completely new array also works, for example todos equals todos.map with t arrow spread t with completed. AI assistants generally might not remember this reactive assignment rule. For instance might do t.completed equals directly inside todos.forEach and finish code, in this case UI doesn't update. Similarly in complex state management, usage of derived stores can be neglected. As example:&lt;/p&gt;

&lt;p&gt;Import writable derived from svelte/store. Export const cartItems equals writable empty array. Export const cartTotal equals derived cartItems with dollar items arrow items.reduce with sum item arrow sum plus item.price times item.qty starting at 0. This way when items in cart change, total automatically updates. AI might miss that it should use derived instead of just simple forEach loop.&lt;/p&gt;

&lt;p&gt;Failure modes: frequent error related to reactive updates is DOM not renewing due to forgetting variable assignment. Outside above todos example, in situations like obj.property plus equals 1, Svelte thinks obj as a whole didn't change. Additionally when using a store, mixing dollar store versus get store or using structures compiler cannot track gives wrong result. As real case, developers sometimes experience memory leak with store subscriptions not cleaned, for example not canceling subscription in onDestroy. Such cleanups can be overlooked in code written by AI.&lt;/p&gt;

&lt;p&gt;Developer strategies: check reactive dollar colon declarations and direct variable assignments. If object or array modification exists in AI code, ensure relevant variable gets reassigned. If this isn't possible, create new data structures with methods like map or slice doing same operation. Apply derived or subscription subscribe usage correctly in stores. Do manual supervision in complex state flows. For instance test dollar colon blocks, validate cyclic assignments get cleaned. Ultimately you should adapt code to Svelte's reactivity rules in way compiler will understand.&lt;/p&gt;

&lt;p&gt;One of Svelte's most important features is generating code optimized at compile time. This design doesn't require framework with no virtual DOM and lightens runtime. However this approach brings some edge cases. For instance in Svelte 5 version there's bug report about html tag usage, this marker isn't validated after SSR leading to unexpected behavior. AI-assisted code cannot see such version-specific errors so can fall into same trap. Svelte also enables Server-Side Rendering SSR. But some APIs don't work in SSR. For instance Svelte's MediaQuery class cannot give correct value server side because no browser measurement, leading to content change during hydration.&lt;/p&gt;

&lt;p&gt;If this gets overlooked in AI code, production and browser output can be incompatible. Lifecycle hooks also matter. Code inside onMount runs only on client. AI sometimes can put operations requiring DOM access like document or window access in places that shouldn't run in SSR. This creates both error and SEO problem. Example showing only dispensable data requests should be made in load function below. If AI fetches large amount of data inside load, render time gets longer.&lt;/p&gt;

&lt;p&gt;Script context equals module. Export async function load with fetch parameter. Const res equals await fetch /api/posts. Return posts equals await res.json. Script. SvelteKit documentation emphasizes data not needed shouldn't be pulled to client side immediately. This might not be considered in AI code.&lt;/p&gt;

&lt;p&gt;Developer strategies: review Svelte code AI generated considering compile time. Ensure codes requiring SSR are separated with conditions like onMount or dollar client. Check whether custom directives AI uses like use:action and bind are in right place. Track version-specific errors from Svelte announcement pages, for example html problem. For performance, apply code splitting lazy-load where SvelteKit supports dynamic import. In above load example, extra data can be precompiled with prerender or pending properties can be used. Briefly pay attention to runtime and compile-time distinctions avoiding breaking assumptions Svelte is optimized for.&lt;/p&gt;

&lt;p&gt;In applications developed with Svelte, accessibility is provided with semantic HTML and correct ARIA usage. Official documents emphasize all elements requiring user interaction should be accessible and usable with keyboard. For instance in a form when input field created dynamically using bind:this equals inputEl, focus should be set correctly with inputEl.focus function. AI assistants might skip focus management and event properties like Enter and Escape key. In MDN Svelte example, user experience improved by calling cancel function when Escape key pressed with on:keydown. Such keyboard shortcuts generally don't get added to AI outputs.&lt;/p&gt;

&lt;p&gt;Another point is usage of ARIA attributes and semantic tags. When selecting tag names in Svelte components, screen reader compatibility should be considered. For instance real button should be used instead of div role equals button. Example situation: in modal or popup component, aria-modal equals true and role equals dialog should be added and focus trap precautions taken. This is generally missing in AI code. Additionally attributes like aria-label and aria-labelledby in HTML tags must definitely be added if buttons or links aren't clear. In MDN advanced Svelte guide, focus order arranged so focus loss doesn't happen with tab key. AI outputs might not pay attention to focus shifts.&lt;/p&gt;

&lt;p&gt;Developer strategies: adhere strictly to semantic HTML usage and ARIA guides. Every tag like button, input, form in AI code's needed accessibility attribute like aria attributes and label-id association should be checked. For focus management and keyboard access, handlers like tabindex and on:keydown should be added. Consciously use focus-visible in places where focus emphasis gets lost with CSS. After getting reference to DOM nodes with bind:this in Svelte, focus giving strategy should be considered in component lifecycle like onMount. Don't forget to manually provide these details AI might bypass.&lt;/p&gt;

&lt;p&gt;In Svelte applications package size and build optimization carry great importance. Svelte's compiler structure provides tree-shaking built-in, but external dependencies should still be checked in bundler configuration. For instance AI code frequently generates by importing entire large libraries like import underscore from lodash, whereas with proper structuring only needed functions should be included like import uniq from lodash-es. In SvelteKit documents, noted that code with static import will load with page. If lazy load forgotten in AI outputs, initial load grows unnecessarily. In developer blog above, dynamic loading of components like modals or admin panels with import inside onMount was recommended. AI-assisted structures not doing this worsen performance.&lt;/p&gt;

&lt;p&gt;Another matter mentioned in SvelteKit performance notes is version updates. Svelte 5 is faster and smaller than Svelte 4, Svelte 4 also faster and smaller than 3 is recommended. This is an optimization missed when AI assistants recommend old version codes or don't track updates. Additionally AI generally skips package analysis. As developer should examine bundle size with tools like rollup-plugin-visualizer and vite-bundle-visualizer, replace large dependencies.&lt;/p&gt;

&lt;p&gt;Example showing modal loading with dynamic code splitting. Script. Import onMount from svelte. Let ModalComponent. onMount async arrow function. Const module equals await import ./Modal.svelte. ModalComponent equals module.default with Modal component now loaded. Script. If ModalComponent. svelte:component this equals ModalComponent. If. With this method, code segmentation done instead of always import Modal from ./Modal.svelte in code from AI.&lt;/p&gt;

&lt;p&gt;Developer strategies: manually check package imports in AI code, prefer including only needed functions. Make dynamic import usage mandatory in compiler and bundler settings. For instance in SvelteKit load functions, pull only critical data instead of loading everything and create static page with prerender equals true. For interactive elements like image lazy load and UI components, use built-in loading equals lazy attributes. Finally during production pre-build process like npm run build, check build minify true settings. These optimizations AI will overlook should be applied manually.&lt;/p&gt;

&lt;p&gt;The following table presents general comparison in Svelte context between AI generation and human generation. Correctness low prone to reactivity and lifecycle errors, can make SSR cliche errors like html versus High with compiler warnings considered and edge cases tested. Accessibility medium with focus order and ARIA attribute generally forgotten versus Good with semantic element and keyboard navigation properly set.&lt;/p&gt;

&lt;p&gt;Maintainability low with repetitions and lack of explanation in auto-generated code pieces versus High with code component-based organized and enriched with comment lines. Package Size large high due to unnecessary dependencies and static imports versus Low minimal with only needed modules and dynamic imports. Brand Fidelity weak with CSS and theme compatibility, style guide inconsistency visible versus High with configuration compliant with corporate style rules provided.&lt;/p&gt;

&lt;p&gt;The mermaid flowchart shows developer checking Svelte code from AI primarily for component structure and props/emits. Then reactive rules like variable assignments get inspected. SSR and CSR distinction like onMount and dollar browser usage gets reviewed. Next step includes accessibility with keyboard navigation and ARIA and performance optimizations including bundle size and lazy-loading. At each step when deficiency detected, correction made, at end of process code approved. Details AI skipped get completed with manual review.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sve</category>
      <category>javascript</category>
      <category>frontendchallenge</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>DevUnionX</dc:creator>
      <pubDate>Tue, 10 Mar 2026 21:22:38 +0000</pubDate>
      <link>https://future.forem.com/devunionx/-m92</link>
      <guid>https://future.forem.com/devunionx/-m92</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/devunionx" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3180316%2F804c7ae5-1a93-4c38-b9ec-023a59a621a8.jpg" alt="devunionx"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/devunionx/vuejs-future-new-language-of-the-web-ecosystem-1b7a" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;VueJs future new language of the Web ecosystem?&lt;/h2&gt;
      &lt;h3&gt;DevUnionX ・ Mar 10&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#vue&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
