<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Avery</title>
    <description>The latest articles on DEV Community by Avery (@avery_code).</description>
    <link>https://dev.to/avery_code</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3837121%2F51bc1289-fc3a-49a8-ace7-d5052dd80cd9.png</url>
      <title>DEV Community: Avery</title>
      <link>https://dev.to/avery_code</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/avery_code"/>
    <language>en</language>
    <item>
      <title>A Folder Per Type or a Folder Per Feature. Only One of Them Survives an AI Session.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:19:04 +0000</pubDate>
      <link>https://dev.to/avery_code/a-folder-per-type-or-a-folder-per-feature-only-one-of-them-survives-an-ai-session-c67</link>
      <guid>https://dev.to/avery_code/a-folder-per-type-or-a-folder-per-feature-only-one-of-them-survives-an-ai-session-c67</guid>
      <description>&lt;p&gt;I was looking for a component called &lt;code&gt;StatusBadge&lt;/code&gt; a while back, and the search turned up four different import paths pointing at the same file. One went through a relative path with three levels of &lt;code&gt;../&lt;/code&gt; in it. One imported it from a barrel file two directories up. One went directly at the file. One came through a re-export in a completely unrelated feature that had apparently needed it once and exported it again on the way through.&lt;/p&gt;

&lt;p&gt;Every one of those imports worked. The bundler resolved all four, TypeScript was happy, nothing was broken in any way a test would catch. But if you wanted to move that file, you now had four different kinds of reference to fix, and no reliable way to know you had found all of them.&lt;/p&gt;

&lt;p&gt;This is part two of a series on writing a rule file for React. Part one covered state. This one covers the structural layer, because the shape of your folders decides how much any of your other rules can actually accomplish.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two structures and why the choice matters more now
&lt;/h2&gt;

&lt;p&gt;The first structure is organising by type. Components in a components folder, hooks in a hooks folder, services in a services folder, types in a types folder. It is the structure most tutorials use and the one most projects start with, because at twelve files it is obviously correct and requires no thinking at all.&lt;/p&gt;

&lt;p&gt;The second is organising by feature. Every feature owns a folder, and inside that folder sit its components, its hooks, its services, its types. Shared code exists, but only for things genuinely used across multiple features.&lt;/p&gt;

&lt;p&gt;Before AI was writing a meaningful share of the code, this was mostly a preference argument. Both structures work. Plenty of successful projects use each one. The type-based version gets awkward around a hundred files, the feature-based version has more upfront ceremony, and teams picked whichever fit how they thought about the product.&lt;/p&gt;

&lt;p&gt;What changed is that the AI has to place every new file it creates, and it makes that decision from whatever it can see in the immediate context of that one session. In a type-based structure there is very little in the context that says where the new hook belongs, because the hooks folder holds forty unrelated hooks and none of them tell you anything about this one. In a feature-based structure the answer is usually sitting right there in the same folder as the thing being extended.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the type-based structure does across enough sessions
&lt;/h2&gt;

&lt;p&gt;The components folder grows without any grouping, so after a few months you have eighty files in one directory and finding anything means knowing the exact name in advance.&lt;/p&gt;

&lt;p&gt;Imports start reaching sideways because there is no boundary saying they should not. A component in one part of the product imports a hook written for a different part, because the hook is sitting in the same hooks folder and looks applicable. That works fine until someone changes the hook for its original purpose and breaks something three features away that nobody remembered was connected.&lt;/p&gt;

&lt;p&gt;Relative paths get deep. When related files sit in four different top-level folders, the path between them climbs up and back down, and the AI writes whatever path resolves. Nothing in a type-based layout discourages &lt;code&gt;../../../&lt;/code&gt;, so it accumulates.&lt;/p&gt;

&lt;p&gt;The part that surprised me most was how much it undermines the state rules from part one. Rule one says state moves to a hook in the feature when two components in that feature need it. That rule assumes a feature is a place you can point at. In a type-based structure it is a concept in your head, so the AI reads the rule, has no folder to put the hook in, and drops it into the general hooks folder alongside everything else, where the next feature will eventually import it and the boundary the rule was protecting stops existing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture section
&lt;/h2&gt;

&lt;p&gt;Five rules. Same file as part one, section below it.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Architecture and Files

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Structure is feature based. Each feature folder contains its&lt;br&gt;
own components, hooks, services and types. Technical folders&lt;br&gt;
are allowed only for building blocks genuinely used across&lt;br&gt;
multiple features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every feature exposes a public API through an index file.&lt;br&gt;
Imports from outside the feature go through that file and&lt;br&gt;
nothing else. Files not exported from the index are private&lt;br&gt;
to the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No deep relative paths. If an import needs more than one&lt;br&gt;
level of ../ it is reaching across a boundary that should&lt;br&gt;
have gone through a public API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer order is UI, then hooks, then services, then the API&lt;br&gt;
layer. Each layer talks to the one below it. A component&lt;br&gt;
never calls the API layer directly and never knows what&lt;br&gt;
shape the backend returns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One responsibility per file. Files above roughly 200 to 300&lt;br&gt;
lines get split before any further work continues, and a&lt;br&gt;
refactor is not finished until the imports are consolidated.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
&lt;br&gt;
&lt;br&gt;
Going through them&lt;br&gt;
&lt;/h2&gt;


&lt;p&gt;Rule one is doing something subtle that took me a while to notice. It gives every future file a home that can be worked out from the thing being extended. When the AI adds a hook for a feature, the folder is already sitting next to the component it belongs to. The decision stops being open.&lt;/p&gt;

&lt;p&gt;The sentence about technical folders is the escape hatch, and it needs to be in there or people start putting genuinely shared things into feature folders and importing across boundaries to reach them. A date formatter used by six features belongs in a shared folder. A hook used by one feature does not, even if someone can imagine a second feature wanting it later.&lt;/p&gt;

&lt;p&gt;Rule two is the one that actually holds the structure together, and it is the one people skip because it feels like extra work for no return. The return shows up the first time you refactor the inside of a feature. Everything not in the index file is yours to move, rename or delete, because you can see with certainty that nothing outside the folder depends on it. Without that guarantee every internal change is a search across the whole codebase.&lt;/p&gt;

&lt;p&gt;It also gives the AI a much clearer signal about what is available. When a session looks at a feature from the outside, the index file is a short list of things meant to be used. The alternative is the AI scanning a folder of internal files and picking whichever one looks closest to what the prompt described, which is how a private helper ends up imported into three other places.&lt;/p&gt;

&lt;p&gt;Rule three is the smallest one and I almost folded it into rule two. It stayed separate because it is the one you can check in about four seconds. Deep relative paths are the visible symptom of a boundary being crossed, so a rule that names the symptom directly gets applied more reliably than a rule that only describes the underlying principle. Search your project for &lt;code&gt;../../../&lt;/code&gt; and whatever comes back is a list of places where the structure is not holding.&lt;/p&gt;

&lt;p&gt;Rule four is the layer order, and the last sentence carries most of the value. A component that knows the backend returns &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;full_name&lt;/code&gt; is a component that breaks when the backend renames a field. Once the mapping lives in the service layer, an API change touches one function. I have watched a field rename turn into a fourteen file pull request because the raw shape had leaked upward into components over about a year, and none of those fourteen changes were interesting work.&lt;/p&gt;

&lt;p&gt;Rule five is the file size limit, and the range matters more than the exact number. Two hundred is not meaningfully different from two hundred and fifty. What matters is that there is a point where splitting stops being optional, because "this file is getting long" is not something a session can evaluate consistently and a line count is. The clause about the refactor being unfinished until imports are consolidated is there because half-done splits are worse than no split. You end up with the new files and the old paths still pointing at the old locations through compatibility re-exports that nobody removes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration question
&lt;/h2&gt;

&lt;p&gt;If you are sitting on a type-based structure with a few hundred files, moving everything is not realistic and I would not suggest it.&lt;/p&gt;

&lt;p&gt;What worked for me was applying the rules to new features only, and moving an existing one when it needed real work anyway. The next feature gets a folder with the structure inside it. The one after that too. Anything already there stays where it is until there is a reason to touch it.&lt;/p&gt;

&lt;p&gt;That leaves you with a mixed codebase for a while, which feels wrong and is fine. The alternative is a migration that takes three weeks, produces no user-visible change, and stalls halfway through when something more urgent comes up.&lt;/p&gt;

&lt;p&gt;Part three covers typing and data contracts, which is where the layer rule from this section either holds or quietly falls apart.&lt;/p&gt;




&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Start Your React Rule File With State. Here Is How to Write That Section.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:45:38 +0000</pubDate>
      <link>https://dev.to/avery_code/start-your-react-rule-file-with-state-here-is-how-to-write-that-section-2ngb</link>
      <guid>https://dev.to/avery_code/start-your-react-rule-file-with-state-here-is-how-to-write-that-section-2ngb</guid>
      <description>&lt;p&gt;A friend of mine decided last spring that he was finally going to write down his coding standards so the AI would stop handing him components that looked like they came from four different projects. He opened a markdown file, typed a heading, sat there for maybe ten minutes, and then went and did something else. When I asked him about it a few weeks later the file was still open in a tab somewhere with nothing under the heading.&lt;/p&gt;

&lt;p&gt;That happens to almost everyone who tries this. "Write down your standards" sounds like one task and is actually about forty of them stacked on top of each other. Architecture, naming, typing, accessibility, error handling, folder structure, and no particular reason to start with any one over the others.&lt;/p&gt;

&lt;p&gt;My suggestion is to start with state. It is the section where you find out fastest whether the thing you just wrote is a real rule or a sentence you happen to agree with. This is the first part of a series that works through a rule file one section at a time, and each part hands you something you can drop into your own file the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first rule everybody writes
&lt;/h2&gt;

&lt;p&gt;It usually looks like this.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep state in the right place.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The AI agrees with this completely. It agreed before you typed it. Ask it directly whether state belongs in the right place and you get a reasonable little explanation about lifting state up and colocation, and then it hands you a component with a filter value, a sort order, a modal flag and a fetch all in the same function body, because nothing in that sentence told it where the line falls in this particular case.&lt;/p&gt;

&lt;p&gt;The test I use now took me an embarrassingly long time to arrive at. A rule belongs in the file if a session that reads it produces different code than a session that does not. That is the whole bar. Everything that fails it is a note to yourself, which is fine, but it should live somewhere other than the file the AI reads before every task.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when the rule has an edge
&lt;/h2&gt;

&lt;p&gt;Here is what I ended up with instead.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;State is local if it is only required within one feature or
screen. State becomes global only when at least two independent
features need it and the need is long term.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Roughly the same idea, except now there is something in there you can hold up against actual code. Two independent features. Long term.&lt;/p&gt;

&lt;p&gt;The word independent carries most of the weight, and I only worked that out after the first version of the rule said two components and kept pushing things into the store that had no business being there. Two components inside one feature sharing a value is an ordinary situation with an ordinary answer, which is a hook. Two features that otherwise do not know about each other needing the same data is a different problem with different consequences. If the rule does not separate those cases, then over a few months everything migrates upward into the global store, one reasonable-looking decision at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The section as it stands
&lt;/h2&gt;

&lt;p&gt;Six rules. This is what is actually in the file, not a tidied version for the article.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## State and Effects

1. State is local if it is only required within one feature or
   screen. State becomes global only when at least two independent
   features need it and the need is long term. Auth state is not
   automatically global.

2. There is never more than one mutable copy of the same domain
   data. If two places can both change the same information, one
   of them is wrong. Original data is a snapshot for comparison,
   not a second state source.

3. Derived state is not stored. Anything that can be computed
   from existing state or props is computed during render. Dirty
   flags and comparison results are derived, never persisted.

4. No useEffect for logic that can be synchronously derived from
   props or state. If the effect body only reads state and writes
   state, delete it and compute the value instead.

5. Loading, error, and success are explicit typed states. A
   component that can be in one of them declares that in its
   types rather than inferring it from whether data happens to
   be null.

6. Form components are presentational. Initial values come from
   outside. State transformation happens in the hook, not inside
   the form.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Around two hundred words, which is deliberate. There was a much longer version at one point and I noticed nobody on the team could tell me what was in it, including me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going through them
&lt;/h2&gt;

&lt;p&gt;The sentence about auth in rule one exists because of a specific afternoon I spent untangling an auth store in a client project. Auth goes global on day one in almost every codebase I have worked in, usually before anyone knows which features will actually need it, and once it is sitting there it becomes the argument for the next thing that goes in. Sometimes global genuinely is the right call. If you have a header, a router guard, three feature areas and a permissions system all reading the same user object, that is what a store is for. What you often have instead is a token that one module cares about and a user object read by two screens, and the whole thing could have stayed inside the auth feature behind a hook. Writing the exception into the rule means someone has to make that decision on purpose.&lt;/p&gt;

&lt;p&gt;Rule two prevents the category of bug that costs the most hours. When two places can both mutate the same information they will eventually disagree, and the symptom surfaces nowhere near the cause, so you end up reading four files trying to understand why the name in the header does not match the name on the profile underneath it. The part about snapshots covers the version I have seen most often, which is holding onto the original record to compare against the edited one for a dirty indicator. That original is a reference. The moment it becomes state you have the second mutable copy the rule exists to stop.&lt;/p&gt;

&lt;p&gt;Rule three is derived state, and it is the clearest example I know of the gap between a model knowing something and a model doing it. Ask any of them about storing derived state and you get a correct, well organised answer about single sources of truth. Then in the middle of building a component that fetches a list, filters it, and renders a summary, out comes a &lt;code&gt;total&lt;/code&gt; in useState with an effect keeping it in sync with &lt;code&gt;items&lt;/code&gt;, because at that point in the generation storing the number is simply the cheapest available move. The rule teaches the model nothing it did not already know. What it does is close the door on the cheap move.&lt;/p&gt;

&lt;p&gt;Rule four goes after the same problem from the mechanism side. An effect that only reads state and writes state has no reason to run. It fires on renders that did not need it, it opens a window where the value is one tick behind, and looked at on its own during review it seems completely fine, which is exactly why it survives review.&lt;/p&gt;

&lt;p&gt;Rule five is smaller than the others and I nearly left it out. Inferring loading from a null check works right until null becomes a legitimate value in your data, or until somebody asks you to tell an empty result apart from a failed request. Then you add a second flag and now two things describe one situation. Putting the state into the type means the component cannot quietly forget a case, because it will not compile.&lt;/p&gt;

&lt;p&gt;Rule six is about forms because forms are the first place the separation between presentation and logic falls apart. A form that owns its initial values and does its own transformation cannot be reused for the edit case, cannot be tested without mounting it, and over time turns into the file where business logic goes to hide. Keeping the transformation in the hook is what keeps a form a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;There was a rule about prefixing boolean state with &lt;code&gt;is&lt;/code&gt; or &lt;code&gt;has&lt;/code&gt;. Perfectly good rule. It belongs in the naming section, and once it was in two places I could see exactly how the two copies were going to drift apart over the next year.&lt;/p&gt;

&lt;p&gt;There was also a longer one about reaching for useReducer instead of several useState calls. Every version I wrote either repeated something rule one already covered or leaned on a threshold I could not actually defend when someone asked me why that number. It never once changed an output, so it went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the section before you commit to it
&lt;/h2&gt;

&lt;p&gt;Take a component the AI generated recently with no rules in place. Paste these six in front of a prompt describing the same feature and generate it again.&lt;/p&gt;

&lt;p&gt;Then look specifically at what the rules cover. Where did the state land. Is anything stored that could have been computed. Are there two places holding the same information. Does the loading state come from a type or from a null check.&lt;/p&gt;

&lt;p&gt;If the second version looks basically the same as the first, the rules are not specific enough for your codebase yet, and the fix is sharpening the conditions rather than adding more rules on top. Six rules with edges beat twenty vague ones, and the vague ones cost you attention on the way to the useful ones.&lt;/p&gt;

&lt;p&gt;The next part covers architecture and file boundaries, which turns out to decide how much these state rules can accomplish in the first place.&lt;/p&gt;




&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>What Does a React Component Actually Look Like Before and After Rules?</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Fri, 04 Sep 2026 13:48:26 +0000</pubDate>
      <link>https://dev.to/avery_code/what-does-a-react-component-actually-look-like-before-and-after-rules-44b6</link>
      <guid>https://dev.to/avery_code/what-does-a-react-component-actually-look-like-before-and-after-rules-44b6</guid>
      <description>&lt;p&gt;Most of what gets written about AI coding standards stays at the level of principle. Define your architecture. Be explicit about naming. Decide where state lives. All reasonable advice, and all of it fairly abstract until you sit down and actually look at two versions of the same file.&lt;/p&gt;

&lt;p&gt;So here is the concrete version. One component, generated by the AI without any project rules in place, then the same component built again with three specific rules provided beforehand. Same feature, same requirements, same model. The only variable is whether the rules existed before generation started.&lt;/p&gt;

&lt;p&gt;The feature is a user list with search and a status filter. Nothing exotic. The kind of thing that shows up in almost every internal tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component without rules
&lt;/h2&gt;

&lt;p&gt;This is roughly what came back the first time, with a prompt that described the feature and nothing else.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(false)
  const [search, setSearch] = useState('')
  const [status, setStatus] = useState('all')
  const [error, setError] = useState(null)

  useEffect(() =&amp;gt; {
    setLoading(true)
    fetch('/api/users')
      .then(res =&amp;gt; res.json())
      .then(data =&amp;gt; {
        setUsers(data.users.map(u =&amp;gt; ({
          id: u.user_id,
          name: u.full_name,
          email: u.email_address,
          status: u.account_status
        })))
        setLoading(false)
      })
      .catch(err =&amp;gt; {
        setError(err.message)
        setLoading(false)
      })
  }, [])

  const filtered = users.filter(u =&amp;gt; {
    const matchesSearch = u.name.toLowerCase().includes(search.toLowerCase())
    const matchesStatus = status === 'all' || u.status === status
    return matchesSearch &amp;amp;&amp;amp; matchesStatus
  })

  if (loading) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;
  if (error) return &amp;lt;div&amp;gt;Error: {error}&amp;lt;/div&amp;gt;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input value={search} onChange={e =&amp;gt; setSearch(e.target.value)} /&amp;gt;
      &amp;lt;select value={status} onChange={e =&amp;gt; setStatus(e.target.value)}&amp;gt;
        &amp;lt;option value="all"&amp;gt;All&amp;lt;/option&amp;gt;
        &amp;lt;option value="active"&amp;gt;Active&amp;lt;/option&amp;gt;
        &amp;lt;option value="inactive"&amp;gt;Inactive&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;
      {filtered.map((u, i) =&amp;gt; (
        &amp;lt;div key={i}&amp;gt;
          &amp;lt;span&amp;gt;{u.name}&amp;lt;/span&amp;gt;
          &amp;lt;span&amp;gt;{u.email}&amp;lt;/span&amp;gt;
          &amp;lt;span&amp;gt;{u.status}&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This code works. If you drop it into a project it renders, it filters, it handles the loading and error cases. Nothing here would throw an exception in normal use.&lt;/p&gt;

&lt;p&gt;It also does four different jobs in one file. It fetches data, it transforms the API response into a different shape, it manages filter state and computes the filtered result, and it renders the UI. The API field names get mapped inline inside the fetch callback, which means the component knows the exact shape the backend returns. The list uses the array index as a key, which will cause React to misidentify rows the moment filtering changes the order. The loading state is bare text.&lt;/p&gt;

&lt;p&gt;None of these are things the AI does not know about. Ask it directly whether index as key is a good idea and it will explain why it is not. The knowledge is there. What is missing is anything forcing that knowledge to apply during generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three rules
&lt;/h2&gt;

&lt;p&gt;These are the rules that went in before the second attempt. Deliberately short, because a first rule set that takes an afternoon to write never gets written.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Components render only. Data fetching lives in a service function.
   State logic lives in a custom hook. A component that fetches,
   transforms, and renders is three things pretending to be one.

2. API responses get mapped to domain objects in the service layer,
   before the data reaches any hook or component. Nothing above the
   service layer knows what the backend field names are.

3. Every list item key is a stable identifier from the data itself.
   If the data has no stable id, generate one during the service
   layer mapping, never inline in the render.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three rules. No architecture document, no folder structure mandate, nothing about naming conventions yet. Just the three decisions that address what actually went wrong in the first version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same component with the rules in place
&lt;/h2&gt;

&lt;p&gt;The AI split the work into three files without being told to split it into three files. That happened because rule one made the boundaries explicit.&lt;/p&gt;

&lt;p&gt;First, the service:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// services/userService.js

export async function fetchUsers() {
  const res = await fetch('/api/users')
  if (!res.ok) {
    throw new Error(`Failed to load users: ${res.status}`)
  }
  const data = await res.json()
  return data.users.map(toUser)
}

function toUser(raw) {
  return {
    id: raw.user_id,
    name: raw.full_name,
    email: raw.email_address,
    status: raw.account_status
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The mapping moved here, which is rule two doing its job. The backend field names now appear in exactly one place in the codebase. If the API renames &lt;code&gt;full_name&lt;/code&gt; to &lt;code&gt;display_name&lt;/code&gt; next quarter, one function changes and nothing else in the application notices.&lt;/p&gt;

&lt;p&gt;Then the hook:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// hooks/useUserList.js

export function useUserList() {
  const [users, setUsers] = useState([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState(null)
  const [searchTerm, setSearchTerm] = useState('')
  const [statusFilter, setStatusFilter] = useState('all')

  useEffect(() =&amp;gt; {
    let cancelled = false

    fetchUsers()
      .then(result =&amp;gt; {
        if (!cancelled) {
          setUsers(result)
          setIsLoading(false)
        }
      })
      .catch(err =&amp;gt; {
        if (!cancelled) {
          setError(err.message)
          setIsLoading(false)
        }
      })

    return () =&amp;gt; { cancelled = true }
  }, [])

  const visibleUsers = useMemo(() =&amp;gt; {
    const term = searchTerm.toLowerCase()
    return users.filter(user =&amp;gt; {
      const matchesSearch = user.name.toLowerCase().includes(term)
      const matchesStatus =
        statusFilter === 'all' || user.status === statusFilter
      return matchesSearch &amp;amp;&amp;amp; matchesStatus
    })
  }, [users, searchTerm, statusFilter])

  return {
    visibleUsers,
    isLoading,
    error,
    searchTerm,
    setSearchTerm,
    statusFilter,
    setStatusFilter
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Something worth noticing here that no rule asked for. The cleanup flag preventing a state update after unmount appeared in this version and not in the first one. So did the &lt;code&gt;useMemo&lt;/code&gt; around the filter computation, and so did the &lt;code&gt;isLoading&lt;/code&gt; naming with the boolean prefix.&lt;/p&gt;

&lt;p&gt;I did not write rules for any of those three things. My read is that once the hook exists as its own file with a single clear job, the generation is no longer juggling four concerns at once, and the details that get dropped under that load stop getting dropped. Whether that generalizes or whether I got lucky in this particular case, I honestly do not know. It happened consistently enough across the components I rebuilt that I stopped treating it as a coincidence.&lt;/p&gt;

&lt;p&gt;And the component:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// components/UserList.jsx

export function UserList() {
  const {
    visibleUsers,
    isLoading,
    error,
    searchTerm,
    setSearchTerm,
    statusFilter,
    setStatusFilter
  } = useUserList()

  if (isLoading) return &amp;lt;UserListSkeleton /&amp;gt;
  if (error) return &amp;lt;ErrorMessage message={error} /&amp;gt;

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input
        value={searchTerm}
        onChange={e =&amp;gt; setSearchTerm(e.target.value)}
        aria-label="Search users"
      /&amp;gt;
      &amp;lt;select
        value={statusFilter}
        onChange={e =&amp;gt; setStatusFilter(e.target.value)}
        aria-label="Filter by status"
      &amp;gt;
        &amp;lt;option value="all"&amp;gt;All&amp;lt;/option&amp;gt;
        &amp;lt;option value="active"&amp;gt;Active&amp;lt;/option&amp;gt;
        &amp;lt;option value="inactive"&amp;gt;Inactive&amp;lt;/option&amp;gt;
      &amp;lt;/select&amp;gt;

      &amp;lt;ul&amp;gt;
        {visibleUsers.map(user =&amp;gt; (
          &amp;lt;li key={user.id}&amp;gt;
            &amp;lt;span&amp;gt;{user.name}&amp;lt;/span&amp;gt;
            &amp;lt;span&amp;gt;{user.email}&amp;lt;/span&amp;gt;
            &amp;lt;span&amp;gt;{user.status}&amp;lt;/span&amp;gt;
          &amp;lt;/li&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The component is now twenty lines of rendering. &lt;code&gt;user.id&lt;/code&gt; as the key came directly from rule three. The &lt;code&gt;ul&lt;/code&gt; and &lt;code&gt;li&lt;/code&gt; elements, the aria labels on the inputs, the skeleton instead of the text placeholder, none of that was in the rules either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison actually tells you
&lt;/h2&gt;

&lt;p&gt;The first version was not badly written. It was written without a defined boundary, and everything else followed from that.&lt;/p&gt;

&lt;p&gt;When a single function is responsible for fetching, mapping, filtering, and rendering, the generation is making all of those decisions in one pass. The API mapping goes inline because that is where the fetch happens to be. The key goes to the index because the render is the fourth thing in a chain of concerns and the identifier question does not get much attention by the time it comes up. The loading state is text because it is a detail inside a component that already has too many jobs.&lt;/p&gt;

&lt;p&gt;Three rules changed what the code looks like far more than three rules should be able to. That is what makes this worth actually running yourself rather than taking my word for it, because the multiplier effect is the part that does not come across in the abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on something you already have
&lt;/h2&gt;

&lt;p&gt;Take a component in your project that the AI generated in a single session. Copy the three rules above, paste them in front of a prompt describing the same feature, and generate it again from scratch.&lt;/p&gt;

&lt;p&gt;The second version will not be perfect. Mine was not either. But the comparison tells you something specific and immediately actionable, which is which of your recurring corrections are actually the AI missing knowledge, and which ones are just the absence of a boundary that would have made the correct choice obvious. In my experience it is almost entirely the second category, and that category is the one you can fix in an afternoon.&lt;/p&gt;




&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>I Filled Out Three Forms in My Own App and Got Three Completely Different Validation Experiences.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:11:58 +0000</pubDate>
      <link>https://dev.to/avery_code/i-filled-out-three-forms-in-my-own-app-and-got-three-completely-different-validation-experiences-l06</link>
      <guid>https://dev.to/avery_code/i-filled-out-three-forms-in-my-own-app-and-got-three-completely-different-validation-experiences-l06</guid>
      <description>&lt;p&gt;I was doing a manual walkthrough of a few user flows before a release, clicking through screens as an actual user would rather than testing individual components in isolation. Somewhere between the second and third form I filled out, something started feeling off in a way I could not immediately name.&lt;/p&gt;

&lt;p&gt;The signup form validated aggressively as I typed. The moment I entered a single character in the email field, it turned red and told me the email was invalid, which was technically true but felt hostile given that I had typed exactly one letter of what would eventually be a complete address. The settings form did the opposite. It stayed completely silent through everything I typed, then dumped four separate error messages on me at once when I hit save, forcing me to scroll back up and figure out which fields it was actually complaining about. The contact form did something in between, waiting until I moved out of each field before evaluating it, which felt notably more reasonable than either of the others.&lt;/p&gt;

&lt;p&gt;Three forms in one application. Three fundamentally different validation experiences. From a user's perspective, this made the product feel inconsistent in a way that was difficult to articulate but easy to notice, a sense that different parts of the app had been built by different people with different ideas about how software should behave.&lt;/p&gt;

&lt;p&gt;Every one of these three approaches is a documented, legitimate validation strategy. None of them represents a technical mistake. But encountering all three across a single user session is not a series of independent technical decisions. It is the absence of a decision entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why validation timing fragments more visibly than most inconsistencies
&lt;/h2&gt;

&lt;p&gt;Most of the AI generated inconsistencies worth examining live entirely inside the codebase. Prop drilling handled three different ways, dependency arrays that are sometimes over-specified and sometimes under-specified, naming conventions that drift across sessions. These are real problems with real costs, but the costs are borne almost entirely by developers reading and maintaining the code. Users never encounter them directly.&lt;/p&gt;

&lt;p&gt;Validation timing is different because it crosses directly into the user experience. The choice between validating on change, on blur, or on submit is not primarily a code structure decision. It is a decision about how the product communicates with the person using it, when it interrupts them, and how much patience it extends before flagging something as wrong.&lt;/p&gt;

&lt;p&gt;This means validation inconsistency has two costs stacked on top of each other. There is the usual maintenance cost of having three different implementations of conceptually the same behavior. And there is a product cost that shows up in how the application feels to use, which is harder to measure but often more consequential for whether people actually enjoy using the thing you built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three patterns and what each one is actually good for
&lt;/h2&gt;

&lt;p&gt;Validating on change, meaning evaluating the field on every keystroke, provides the fastest feedback loop. This works genuinely well for certain specific cases, particularly password strength indicators where the user benefits from watching the requirement get satisfied in real time, or character counters where live feedback is the entire point of the feature.&lt;/p&gt;

&lt;p&gt;It works badly for almost everything else, particularly for fields with a format that is only valid once fully entered. Email addresses are the canonical example. Every partially typed email is technically invalid, which means validating on change produces an error state that persists for most of the time the user is typing, only resolving at the very end. The user has done nothing wrong, but the interface has been telling them they have for the entire duration of their input.&lt;/p&gt;

&lt;p&gt;Validating on blur, meaning evaluating when the user leaves the field, is the pattern that tends to feel most natural for standard text inputs. The user gets to finish their thought before being evaluated, but they still receive feedback before submitting the entire form, which means they can correct issues incrementally rather than encountering all of them at once at the end.&lt;/p&gt;

&lt;p&gt;Validating on submit, meaning waiting until the user attempts to submit the whole form, has legitimate uses primarily for validations that cannot be performed on a single field in isolation. Cross-field validations, where the validity of one field depends on the value of another, or validations requiring a server round trip that would be wasteful to trigger on every blur, are reasonable candidates for submit-time evaluation.&lt;/p&gt;

&lt;p&gt;The problem is not that any of these three is wrong. It is that they solve different problems, and using them interchangeably based on which session generated which form means the choice has nothing to do with which problem the specific form actually presents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the AI defaults differently across sessions
&lt;/h2&gt;

&lt;p&gt;The pattern selected in any given session appears to correlate with fairly incidental characteristics of how the form was described in the prompt and what the surrounding code happened to look like.&lt;/p&gt;

&lt;p&gt;A form described with detailed field requirements, particularly requirements involving format constraints, tends to produce validate-on-change behavior, since the format constraints are prominent in the prompt and immediate validation seems responsive to them. A form described more simply, focused on structure rather than validation rules, tends to produce validate-on-submit behavior, since submit-time validation is the minimum implementation that technically satisfies the requirement to validate at all. A form generated in a session where an existing form using blur validation was visible in context is more likely to also use blur validation, though this only holds when the existing example happens to be in the visible context window.&lt;/p&gt;

&lt;p&gt;None of these tendencies are unreasonable as local heuristics. The AI is responding sensibly to the information available in each individual session. But the information available in each individual session varies for reasons entirely unrelated to what the correct validation strategy for that particular form actually is, which is why the outcome varies too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The user experience cost that does not appear in code review
&lt;/h2&gt;

&lt;p&gt;A code reviewer looking at a form component with validate-on-change behavior sees a functioning implementation of a legitimate pattern. There is nothing to flag. The validation works, the errors display correctly, the form submits when valid. Every technical criterion is satisfied.&lt;/p&gt;

&lt;p&gt;What the code reviewer does not see, because it is not visible in a single component in isolation, is that the form two screens over validates completely differently, and that a user moving between those two screens experiences the product as inconsistent in a way that erodes trust in the overall quality of the application.&lt;/p&gt;

&lt;p&gt;This is the specific reason validation timing benefits from being an explicit rule rather than a per-component judgment call. The decision is not visible at the level where code review operates. It is only visible at the level of the complete user experience, which no individual component review ever examines. Rules can operate at that higher level in a way that component-by-component review structurally cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a validation timing rule needs to specify
&lt;/h2&gt;

&lt;p&gt;The rule has to make the timing decision based on the type of field and validation involved, rather than leaving it as an open judgment call for each new form.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Form validation timing rule:
1. Standard text inputs with format requirements, including email, phone, and URL fields, validate on blur. Do not validate these on change, since partial input is necessarily invalid and flagging it produces a hostile experience.
2. Fields where live feedback is the actual feature, specifically password strength indicators and character counters, validate on change. This is the narrow exception, not the default.
3. Cross-field validations, where one field's validity depends on another field's value, evaluate on submit, since evaluating them earlier produces confusing intermediate states that resolve themselves as the user continues.
4. Validations requiring a server round trip evaluate on blur at the earliest, never on change, to avoid generating a request per keystroke.
5. On submit, all fields are re-validated regardless of their individual timing strategy, and focus moves to the first field with an error so the user does not have to search for what went wrong.
6. Error message placement and styling is identical across every form in the application, regardless of which timing strategy applies to that particular field.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The core of this rule is that timing is determined by the nature of the field, not by the session that happened to generate it. An email field validates on blur whether it appears in a signup form, a settings form, or a contact form, because the reasoning about why blur is correct for email fields does not change based on which form the field lives in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why rule five matters more than it initially appears
&lt;/h2&gt;

&lt;p&gt;The fifth rule, about re-validating everything on submit and moving focus to the first error, addresses a failure mode that the other rules do not cover on their own.&lt;/p&gt;

&lt;p&gt;Even with correct per-field timing, a user can reach the submit button with errors present, particularly if they never focused certain fields at all, meaning blur validation never triggered for those fields. Without explicit submit-time re-validation, these fields can pass through unvalidated, or produce errors that the user cannot easily locate on a long form.&lt;/p&gt;

&lt;p&gt;Moving focus to the first error is a small detail that has an outsized effect on how forgiving the form feels, particularly on longer forms where the error might be several screens above where the submit button sits. This is exactly the kind of detail that gets skipped when validation is implemented fresh in each session, since it is not necessary for the form to technically function and requires deliberate additional implementation effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after the rule went into effect
&lt;/h2&gt;

&lt;p&gt;New forms consistently used blur validation for standard fields, which was the pattern that had previously appeared least often despite being the most appropriate for the majority of cases. Validate-on-change stopped appearing for email and similar format-constrained fields entirely, which eliminated the specific experience of being told an email was invalid after typing one character.&lt;/p&gt;

&lt;p&gt;The submit-time re-validation and focus management specified in rule five turned out to require the most implementation change, since it had been implemented inconsistently or not at all in most existing forms. This was the part that produced the most noticeable improvement in how forms actually felt to use, which was somewhat unexpected given that the original problem I noticed was about timing rather than error recovery.&lt;/p&gt;

&lt;p&gt;Existing forms with inconsistent timing did not update themselves, and the older forms remain inconsistent until they get touched for other reasons. But new forms consistently follow the pattern, which means the inconsistency is bounded rather than growing with each new form the project adds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Validation timing is a decision about how your product communicates with the people using it, not just a technical choice about when a function runs. Leaving it as a per-session judgment call means the user experience of your forms varies based on how each form's prompt happened to be phrased, which is not a defensible basis for a product decision that users directly experience.&lt;/p&gt;

&lt;p&gt;Look for the other decisions in your project that live at the boundary between code and user experience, where the inconsistency is invisible in individual component review but clearly visible to someone using the actual application. Those are the decisions that most benefit from being an explicit rule, precisely because no existing review process is structured to catch them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project has user-facing decisions that were never standardized?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural gaps where inconsistency is invisible in code review but obvious to anyone actually using the application.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancoding</category>
    </item>
    <item>
      <title>I Counted Three Different Loading Patterns Across Five Components. All Generated by the Same AI.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Fri, 28 Aug 2026 07:59:40 +0000</pubDate>
      <link>https://dev.to/avery_code/i-counted-three-different-loading-patterns-across-five-components-all-generated-by-the-same-ai-3060</link>
      <guid>https://dev.to/avery_code/i-counted-three-different-loading-patterns-across-five-components-all-generated-by-the-same-ai-3060</guid>
      <description>&lt;p&gt;I was cleaning up a settings page and needed to reference how loading states were handled elsewhere in the project, so I could match the existing pattern. That search turned into something more revealing than the task I had actually started with.&lt;/p&gt;

&lt;p&gt;The dashboard component showed a spinner, a simple rotating icon centered in the content area while data loaded. The user profile component showed a skeleton, gray rectangular placeholders roughly matching the shape of the content that would eventually appear. The notifications panel showed plain text, just the word "Loading" sitting where the list would normally render. Three different components, three completely different approaches to communicating the exact same thing to the user, all within the same project, all generated across various sessions with the same AI tool.&lt;/p&gt;

&lt;p&gt;None of these three approaches is wrong. A spinner is a perfectly reasonable way to indicate loading. So is a skeleton. So is plain text, in the right context. Every one of them is a defensible engineering decision if you evaluate it in isolation. But finding all three used interchangeably across a handful of components in one project is not really about any individual decision being wrong. It is about there being no consistent decision at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why loading states specifically tend to fragment this way
&lt;/h2&gt;

&lt;p&gt;Loading states share the same underlying characteristic that made prop drilling and dependency arrays interesting cases to examine. There is no single objectively correct pattern. Spinners, skeletons, and plain text messages are all legitimate solutions, and which one is actually best depends on factors specific to the situation rather than a universal React rule.&lt;/p&gt;

&lt;p&gt;A skeleton tends to work well when the eventual content has a predictable, consistent shape and layout, since the placeholder can closely mirror what will replace it, reducing the visual jump when the real content arrives. A spinner tends to work well for shorter, less predictable loading periods, or for content whose final shape varies too much for a meaningful skeleton to represent. Plain text tends to get used, often somewhat lazily, in situations where the developer or the AI has not put much thought into the loading experience at all, treating it as a placeholder to fill a technical requirement rather than a genuine design decision.&lt;/p&gt;

&lt;p&gt;Because all three have legitimate use cases and none of them is flagged as incorrect the way a missing dependency or an index-as-key violation would be, there is nothing in the AI's general knowledge pushing it toward consistency across different sessions. Each individual loading state gets decided fresh, based on whatever seems reasonable for that specific component in that specific moment, with no memory of how the last three loading states in this same project were handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What determines which pattern gets chosen in any given session
&lt;/h2&gt;

&lt;p&gt;The choice does not appear to be random exactly, but it is heavily influenced by session-specific factors that have nothing to do with what the rest of the project actually does.&lt;/p&gt;

&lt;p&gt;If the component being generated has a clear, well defined data shape visible in the prompt or the surrounding context, a skeleton becomes more likely, since the AI can reasonably infer what the loading placeholder should approximate. If the data shape is vague or the component handles several different kinds of content depending on state, a spinner becomes more likely as the safer, shape-agnostic default. If the loading state feels like an afterthought relative to the main focus of the generation request, plain text becomes more likely, since it requires the least additional code and the least additional thought to include.&lt;/p&gt;

&lt;p&gt;None of these tendencies are wrong reasoning in isolation. They are locally sensible heuristics. The problem is that these session-specific factors, the data shape visible in that particular prompt, how central the loading state was to that particular request, vary constantly across a real project, which means the resulting pattern varies constantly too, even though the actual user-facing requirement, communicate that something is loading, never changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this particular inconsistency matters more than it initially seems
&lt;/h2&gt;

&lt;p&gt;It would be easy to dismiss loading state inconsistency as a minor cosmetic issue, since the core functionality still works regardless of which pattern gets used. But this specific inconsistency has a few consequences worth taking seriously.&lt;/p&gt;

&lt;p&gt;First, it directly affects product polish in a way users notice, even if they cannot articulate why. An application where every section loads with a visually distinct pattern reads as unfinished or cobbled together, even if every individual screen functions correctly. Users build an implicit expectation from their first few interactions with a product, and violating that expectation repeatedly creates a low-grade sense that the product was not built with much care, regardless of how solid the underlying functionality actually is.&lt;/p&gt;

&lt;p&gt;Second, it creates real engineering overhead beyond the visual inconsistency. Three different loading patterns typically mean three different sets of supporting code, three different approaches to accessibility for screen readers announcing loading state, and three different places where a design change to loading behavior has to be implemented separately rather than once. What could be a single shared component handling loading consistently across the application instead becomes three or more bespoke implementations that all have to be maintained independently.&lt;/p&gt;

&lt;p&gt;Third, and this is the part that connects back to the broader pattern across all of these inconsistency examples, it signals that no design system decision was ever actually made for this category of UI. Loading states are exactly the kind of small, easy to overlook decision that benefits enormously from being decided once, explicitly, rather than being left as an ambient judgment call that gets re-litigated every time a new component happens to need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a rule for this actually needs to specify
&lt;/h2&gt;

&lt;p&gt;Unlike the dependency array problem, where the rule needed to force a reasoning process rather than restate a fact, the loading state problem needs a rule that makes an explicit design decision and then applies it consistently, since there genuinely is no single correct answer waiting to be discovered through more careful analysis.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading state consistency rule:
1. Content with a predictable, consistent layout uses a skeleton component that approximates the shape of the eventual content. This applies to lists, cards, and any content type that appears repeatedly with a stable visual structure.
2. Content without a predictable layout, or content that varies significantly in shape depending on the data, uses a centered spinner rather than attempting to construct a skeleton for an unpredictable shape.
3. Full page or full section loading, where the entire visible area is waiting on data, always uses the skeleton pattern if the eventual layout is known in advance, falling back to a spinner only when the layout genuinely cannot be anticipated.
4. Plain text loading indicators are not an acceptable pattern anywhere in this project. If a component's loading state does not clearly fit the skeleton or spinner criteria above, default to a spinner rather than falling back to unstyled text.
5. All loading states include appropriate accessibility attributes, specifically an aria-live region or an aria-busy attribute, applied the same way regardless of whether the visual pattern is a skeleton or a spinner.
6. Loading state components live in a shared location and get reused rather than reimplemented per feature, so a future change to how loading is communicated only requires updating one place.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This rule does not claim that skeletons are always better than spinners or that spinners are always better than skeletons. It makes a specific decision about which pattern applies under which conditions and removes plain text as an option entirely, since plain text was consistently the pattern that appeared when the least thought had gone into the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why removing an option entirely is sometimes the right move
&lt;/h2&gt;

&lt;p&gt;Most of the rules examined in this series so far have been about specifying which of several legitimate options applies under which conditions, preserving all the options but resolving the ambiguity about when each one is used. Loading states are a case where outright removing one of the options produces a better outcome than trying to define narrow conditions under which it would be acceptable.&lt;/p&gt;

&lt;p&gt;Plain text loading states are rarely a deliberate design choice. They are almost always what happens when a loading state gets added as an afterthought, with minimal effort, because the actual focus of that particular generation session was somewhere else. Trying to write a rule specifying exactly when plain text is the correct choice would be attempting to formalize a pattern that mostly exists because insufficient thought was applied, rather than because it represents a genuinely superior approach in specific circumstances.&lt;/p&gt;

&lt;p&gt;Sometimes the correct rule for an inconsistency is not a nuanced decision tree covering every case. Sometimes it is simply eliminating the option that only ever shows up as a symptom of insufficient attention, and defaulting to one of the two options that were actually deliberately chosen for a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after the rule went into effect
&lt;/h2&gt;

&lt;p&gt;After this rule was applied, new components consistently used either the skeleton or spinner pattern depending on whether the content shape was predictable, following the specific criteria in the rule rather than whatever seemed locally reasonable for that particular generation session. Plain text loading indicators, which had previously appeared in roughly a third of the components that included any loading state at all, stopped appearing entirely in new code.&lt;/p&gt;

&lt;p&gt;The shared loading component location specified in rule six also produced an unexpected secondary benefit. Once loading components lived in one place and got imported rather than reimplemented, a design change made to the skeleton component's styling automatically propagated to every place that used it, rather than requiring a search across the codebase to find and update every bespoke implementation individually.&lt;/p&gt;

&lt;p&gt;The existing inconsistent loading states from before the rule did not fix themselves, the same way the existing prop drilling instances from an earlier example did not retroactively resolve. But the pattern stopped growing, which meant the eventual cleanup effort, whenever it happens, deals with a fixed, bounded amount of inconsistency rather than a continuously expanding one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Loading states do not have a single correct implementation the way a dependency array does, which means the fix here looks different from forcing a reasoning process to happen. It looks like making an explicit design decision once, specifying exactly which pattern applies under which conditions, and removing the low-effort default that was never actually a deliberate choice in the first place.&lt;/p&gt;

&lt;p&gt;Look through your own project for the small, easy to overlook UI decisions that feel too minor to formalize, the kind of thing that seems fine to leave as an ambient judgment call. Loading states are one example. There are usually several more sitting quietly in any codebase that has had AI generating components across enough sessions for these small inconsistencies to accumulate without anyone noticing until they specifically go looking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project has small UI decisions that were never actually standardized?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural gaps where multiple valid patterns exist side by side because no explicit decision was ever made.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Setting the Shape Is the New Skill. Your AI Can Only Work Inside a Shape You Actually Defined.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:30:12 +0000</pubDate>
      <link>https://dev.to/avery_code/setting-the-shape-is-the-new-skill-your-ai-can-only-work-inside-a-shape-you-actually-defined-1n29</link>
      <guid>https://dev.to/avery_code/setting-the-shape-is-the-new-skill-your-ai-can-only-work-inside-a-shape-you-actually-defined-1n29</guid>
      <description>&lt;p&gt;There is a shift happening in what makes a developer valuable, and most conversations about it focus on the wrong half of the equation.&lt;/p&gt;

&lt;p&gt;The argument goes something like this. An AI agent can turn a system design you already hold in your head into working software faster than you could ever type it yourself. Which means the ceiling on what you ship is no longer how fast you write code. It is how well you decide what to build and how to shape it. Where state should live. Where component boundaries fall. What consequences show up months later when the thing you built needs to scale.&lt;/p&gt;

&lt;p&gt;This is true, and it is a genuinely useful reframing of where developer value is heading. The developers pulling ahead are the ones who lead that design instead of handing it off entirely, setting the shape and then letting the agent move fast inside it.&lt;/p&gt;

&lt;p&gt;What gets left unsaid in this framing is the actual mechanism connecting the two halves. Having the judgment is one thing. The agent actually working inside the shape you have in mind is a completely different thing, and the gap between them does not close automatically just because your judgment is good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Judgment that stays in your head does nothing for the agent
&lt;/h2&gt;

&lt;p&gt;You can have excellent instincts about how a React project should be structured. Years of pattern recognition about when state should be local versus shared, when a component is doing too much, what naming makes a codebase legible six months later, how a feature boundary should be drawn so it does not become a maintenance problem down the line.&lt;/p&gt;

&lt;p&gt;None of that judgment transfers to an agent by existing in your head. The agent does not have access to your accumulated pattern recognition. It has access to whatever you actually communicate, in whatever form you communicate it, before or during the session where it generates something.&lt;/p&gt;

&lt;p&gt;If your judgment about component boundaries only exists as an intuition you apply when you personally write code, an agent working inside your codebase has no way to inherit that intuition. It will make its own decision about where the boundary falls, based on general patterns from its training and whatever it can infer from the immediate context, which is not the same as your specific, hard-won judgment about this specific project.&lt;/p&gt;

&lt;p&gt;The shape you are supposed to be setting, according to the argument, only actually gets set if it exists somewhere the agent can read it. Otherwise you have good judgment and an agent that is working inside a shape you never actually communicated, just one it guessed at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "setting the shape" concretely requires
&lt;/h2&gt;

&lt;p&gt;Setting the shape is not a single act. It sounds like one thing when described abstractly, but it decomposes into a specific set of decisions that have to be made explicit, not just held as intuition.&lt;/p&gt;

&lt;p&gt;It means deciding, and writing down, where state lives for different categories of data. Not vaguely knowing that some state should be local and some should be shared, but specifying the actual threshold. State used by exactly one component stays in that component. State used by two or more components within a feature moves to a dedicated hook. State needed across independent features only becomes global, and only when genuinely necessary.&lt;/p&gt;

&lt;p&gt;It means deciding, and writing down, what a component boundary actually looks like in this project. Not a general sense of when something feels too big, but a specific line, whether that is a line count, a responsibility count, or a rule about mixing presentation and logic within the same file.&lt;/p&gt;

&lt;p&gt;It means deciding, and writing down, what the domain vocabulary is. The specific words this project uses for its specific concepts, so that a Customer is always a Customer and never sometimes a User, sometimes a Client, depending on which session happened to generate that particular file.&lt;/p&gt;

&lt;p&gt;Each of these is a piece of judgment that experienced developers already have, usually without having consciously articulated it as a rule. The judgment exists. What does not exist, in most cases, is the explicit written version of that judgment that an agent could actually follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more now than it did before agents did most of the typing
&lt;/h2&gt;

&lt;p&gt;Before agents were generating large portions of a codebase, a developer's judgment applied itself automatically, every time, because the same person who held the judgment was also the one physically writing every line. There was no gap between having the instinct and the instinct showing up in the code, because the instinct and the code came from the same source at the same moment.&lt;/p&gt;

&lt;p&gt;Once an agent is doing a significant share of the actual generation, that automatic application breaks. The judgment and the generation are now two separate things, connected only by whatever got communicated between them. If nothing explicit got communicated, the agent generates based on its own general patterns, and your specific judgment, however good it is, sits unused in your head while the agent makes a different decision than you would have made.&lt;/p&gt;

&lt;p&gt;This is the part of the shift that the shape-setting argument gestures at but does not fully spell out. It is not enough to have good judgment about system design in an agent-driven workflow. The judgment has to survive the transition from your head into a form the agent can actually use, every single session, not just the sessions where you happen to remember to mention it in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developers actually pulling ahead are doing something specific
&lt;/h2&gt;

&lt;p&gt;Watch closely at what the developers who are genuinely getting more out of agents are actually doing, beyond the general description of setting the shape and letting the agent work inside it, and a specific pattern emerges. They are not relying on remembering to communicate their judgment fresh in every prompt. They are converting that judgment into a standing set of constraints that exists independently of any individual session, so that setting the shape happens once, comprehensively, rather than being re-attempted piecemeal every time a new prompt gets written.&lt;/p&gt;

&lt;p&gt;This looks different from prompt engineering, even though the two get conflated. A well crafted prompt communicates intent for the current task. A comprehensive rule system communicates the shape for every task, regardless of how that specific prompt happens to be worded. The developers pulling ahead are not writing increasingly clever prompts. They are doing the harder, less glamorous work of articulating their judgment as an explicit, standing system once, and then letting every subsequent prompt operate inside that already-defined shape.&lt;/p&gt;

&lt;p&gt;Here is roughly what that articulation actually looks like when you sit down and do it, rather than just intending to do it eventually:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shape-defining rules that turn judgment into something an agent can follow:
1. State scope is decided by usage, not convenience. Local to one component by default. A dedicated hook when two or more components in the same feature need it. Global only when at least two independent features genuinely require the same data.
2. A component boundary is crossed when a single file starts handling more than one of the following simultaneously: rendering, data fetching, or business logic. When that happens, split before continuing, do not defer the split to a later cleanup pass.
3. The domain vocabulary for this project is fixed and documented. Deviations are not acceptable even when a different word seems reasonable in isolation.
4. Architectural decisions made for this project apply going forward from the date they were made, even in situations where the existing codebase still shows the old pattern in places that have not yet been touched.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;None of this is complicated once written down. What makes it valuable is that it existed as judgment before it existed as a rule, and writing it down is what actually lets an agent operate inside the shape rather than guessing at one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The risk of good judgment that never gets externalized
&lt;/h2&gt;

&lt;p&gt;There is a specific failure mode worth naming directly, because it is easy to fall into precisely because your judgment is genuinely good. If your instincts about architecture are strong, the code an agent produces without explicit rules will often look reasonable to you on a quick glance, because you are mentally filling in the gaps the agent left, correcting small things automatically without fully registering that you are doing correction work at all.&lt;/p&gt;

&lt;p&gt;This creates a false sense that the shape is being set successfully, when what is actually happening is that your judgment is doing constant, invisible repair work on an agent that never actually received the shape in explicit form. The system appears to be working because your judgment is compensating for its absence in the agent's process, not because the agent genuinely internalized anything.&lt;/p&gt;

&lt;p&gt;The tell is usually correction fatigue that never quite goes away, even after months of working with the same agent on the same project. If your judgment were actually transferred, the corrections needed for a given category of decision would decrease over time as the agent's output increasingly matched the shape. If corrections stay roughly constant no matter how long you have been working together, the judgment was never actually externalized. It was just being applied silently, session after session, by you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Judgment about how software should be shaped is genuinely the skill that matters most right now, and that argument is correct as far as it goes. But judgment that only exists in your head does not shape anything an agent generates. It shapes only the code you personally write, which is an increasingly small share of the total output as agents take on more of the actual generation.&lt;/p&gt;

&lt;p&gt;Setting the shape is not something you do once by having good instincts. It is something you do by converting those instincts into an explicit, standing system that exists independently of any single prompt, and giving the agent access to that system before it generates anything at all.&lt;/p&gt;

&lt;p&gt;Your judgment is the asset. Rules are how that asset actually reaches the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your architectural judgment has never actually been written down for your AI to follow?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural decisions where your instincts are strong but your AI has never received them in a form it can actually use.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Found a useEffect With Three Missing Dependencies Right Next to One With Five Unnecessary Ones. Same Codebase.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:50:51 +0000</pubDate>
      <link>https://dev.to/avery_code/i-found-a-useeffect-with-three-missing-dependencies-right-next-to-one-with-five-unnecessary-ones-40em</link>
      <guid>https://dev.to/avery_code/i-found-a-useeffect-with-three-missing-dependencies-right-next-to-one-with-five-unnecessary-ones-40em</guid>
      <description>&lt;p&gt;I was debugging a stale data issue and traced it back to a useEffect that was missing three dependencies it clearly needed. The effect referenced a filter value, a page number, and a sort order, and the dependency array only listed the filter value. Classic stale closure problem, the kind that produces bugs which are maddening to track down because the code looks correct at a glance and passes every manual test you run against it in the moment.&lt;/p&gt;

&lt;p&gt;While fixing that one, I scrolled up to a different useEffect in the same file. This one had the opposite problem. Five dependencies listed, but at least two of them were objects that got recreated on every render, meaning the effect fired constantly, far more often than it needed to, doing work that should have run once or twice per user interaction but was instead running dozens of times per second during certain interactions, quietly burning performance in a way that would never show up in a quick glance at the code.&lt;/p&gt;

&lt;p&gt;Two useEffect hooks. One file. One clearly under-specified, missing dependencies it needed to function correctly. One clearly over-specified, including dependencies that caused it to fire far more than intended. Neither hook was reasoned through the same way, even though they were written weeks apart in the same codebase, presumably by the same AI working from the same underlying model, on the same general category of problem.&lt;/p&gt;

&lt;p&gt;This is the kind of inconsistency that is easy to miss because each individual effect, viewed in isolation, does not look obviously wrong. The under-specified one runs and appears to work most of the time, failing only in the specific interaction sequence that exposes the stale closure. The over-specified one also runs and appears to work, just less efficiently than it should, which rarely triggers the kind of visible failure that gets flagged in a quick review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why dependency arrays are uniquely prone to this
&lt;/h2&gt;

&lt;p&gt;Most React patterns that show inconsistency across an AI-generated codebase involve a choice between multiple legitimate approaches, the same way prop drilling has several correct solutions depending on context and none of them is objectively wrong. Dependency arrays are different in an important way. There is technically one correct answer for any given effect: include every reactive value the effect body reads, and only those values.&lt;/p&gt;

&lt;p&gt;This makes the inconsistency more surprising than in cases with genuine ambiguity. If there is one correct answer, why does the AI arrive at different, both incorrect, answers in different sessions, sometimes in the same file, sometimes within the same component even?&lt;/p&gt;

&lt;p&gt;The reason comes down to what determining the correct dependency array actually requires. It is not a simple lookup or a pattern match against something memorized. It requires tracing every variable referenced inside the effect body, determining whether each one is reactive, meaning it can change between renders, or stable, meaning it never changes for the lifetime of the component. It requires checking whether any of those variables are objects or functions that get recreated on every render even when their contents are conceptually the same. And it requires deciding whether that recreation actually matters for this specific effect, or whether the value is stable enough in practice even though it fails a strict reference equality check.&lt;/p&gt;

&lt;p&gt;This is a genuinely effortful, multi-step analysis, more so than most people appreciate when they see a two-line effect with a two-item dependency array sitting quietly in a component. Getting it right consistently requires that full trace happening every single time, not just for effects that look complicated on the surface but for the deceptively simple ones too, since those are exactly the ones where a missed dependency slips through because the effect looked too short and too obvious to warrant careful checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different failure modes with two different underlying causes
&lt;/h2&gt;

&lt;p&gt;The under-specified failure, missing dependencies that should be there, tends to happen when the effect body is doing something that does not immediately read as depending on a value, even though it functionally does. A function called inside the effect that internally closes over a piece of state, where the dependency on that state is hidden one layer down inside the function definition rather than visible directly in the effect body. A value pulled from a ref that changes over time, where the changing nature of the ref's contents is easy to overlook because refs are correctly excluded from dependency arrays in the general case, leading to an incorrect assumption that anything touching a ref can be safely omitted. A calculated value derived from something that changes, where the derivation happens through an intermediate variable or a small utility function, and the connection back to the original changing source is not obviously visible when scanning the effect body quickly.&lt;/p&gt;

&lt;p&gt;The over-specified failure, including dependencies that should not be there as-is or that need to be stabilized first, tends to happen with objects and functions specifically, rather than with primitives like strings or numbers. An options object passed into a hook, recreated fresh on every single render because it was defined inline in the component body using object literal syntax rather than being memoized with useMemo. A callback function that gets included in the dependency array because it is technically referenced inside the effect and technically does change identity on every render, without the generation process recognizing that the actual fix is wrapping that callback in useCallback rather than simply including it and accepting the extra re-runs.&lt;/p&gt;

&lt;p&gt;Both failure modes come from the same underlying gap in the reasoning process: correctly determining reactivity requires understanding not just what values are referenced textually inside the effect, but how those values are created upstream and whether their identity is actually stable across renders in practice. That second part, the identity stability question, is where both the under-specification and the over-specification tend to originate, just pulling in opposite directions depending on which piece of that reasoning gets shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this varies so dramatically from session to session
&lt;/h2&gt;

&lt;p&gt;The dependency array analysis is sensitive to exactly how much of the surrounding code the model actually traces through during generation, and that amount of tracing is not consistent across different situations even within the same codebase. If a function used inside the effect is defined a few lines up in the same component, that connection is easy to make because it is locally visible. If the function is imported from a custom hook defined in a completely different file, or if the value comes from several layers of prop passing and transformation before it finally reaches this component, the full trace required to determine reactivity correctly becomes substantially harder to complete reliably during a single generation pass.&lt;/p&gt;

&lt;p&gt;This explains why the same underlying codebase can show wildly different quality in its dependency arrays depending on which specific effect you happen to look at. A simple effect in a simple component, where everything referenced is defined two lines above the effect itself and nothing is imported from elsewhere, is much more likely to get a correct dependency array than an effect buried in a component with several custom hooks, prop drilling, and context consumption feeding into it. The complexity of the surrounding code directly affects how reliably the dependency analysis actually gets completed, even though the underlying React rule for what counts as a correct dependency array does not change at all based on that surrounding complexity.&lt;/p&gt;

&lt;p&gt;This also explains why fixing one bad dependency array and moving on rarely solves the broader pattern. The specific effect gets corrected, but the underlying situation that made the analysis unreliable, complex surrounding code, indirect references, values passed through several layers, remains exactly the same for the next effect that gets generated in similarly complex surroundings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a rule actually needs to specify here
&lt;/h2&gt;

&lt;p&gt;Unlike prop drilling, where the rule needs to define which of several legitimate patterns applies under which conditions, the dependency array problem needs a rule that forces the actual analytical steps to happen rather than being silently skipped when they seem unnecessary for a short, simple-looking effect. The correct answer here is not ambiguous the way prop drilling's correct answer depends on context. The problem is that determining it correctly is effortful enough that the effort sometimes gets shortcut, especially for effects that appear simple at first glance.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dependency array verification rule:
1. Before finalizing any useEffect, explicitly list every variable, function, and value referenced inside the effect body, including anything referenced indirectly through a called function or a value derived from another value.
2. For each referenced value identified in step one, determine whether it is reactive, meaning it can change between renders, or stable, meaning it never changes for the lifetime of the component.
3. Every reactive value identified in step one belongs in the dependency array without exception, regardless of how the effect appears to behave during casual testing.
4. Any object or function included as a dependency must be verified as stable across renders before being included as-is. If it is not stable, it gets wrapped in useMemo or useCallback before being used as a dependency, rather than being included unstable or omitted entirely to avoid extra effect firing.
5. Do not use the exhaustive-deps lint suppression comment under any circumstances. If the technically correct dependency array causes a visible problem, that problem is a signal that something else in the effect needs restructuring, not that the array itself should be shortened to make the symptom disappear.
6. Any effect with more than three dependencies is a signal to reconsider whether the effect is doing too much and should be split into separate effects with narrower, more independent responsibilities.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This rule does not teach the AI new information about how dependency arrays fundamentally work, since that knowledge was already present and demonstrably correct when asked directly. It forces the explicit trace described in steps one and two to happen as a distinct step in the process, rather than allowing the shortcut of an intuitive, pattern-matched guess about what probably belongs in the array based on how similar effects have looked before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why forcing the explicit trace matters more than restating the rule
&lt;/h2&gt;

&lt;p&gt;Simply telling the AI to include all dependencies correctly is restating information it already reliably has, in the same way that telling it not to use array index as a key restates information it already reliably has when asked directly. The actual failure point in both cases is not missing knowledge about what the rule states. It is skipping the effortful trace required to correctly apply that rule to this specific, particular case, under the pressure of generating a larger component quickly.&lt;/p&gt;

&lt;p&gt;A rule that forces the trace to happen as an explicit, named step, rather than trusting that the trace happened silently and correctly somewhere in the generation process, closes the gap in a way that simply restating the rule does not. This is a pattern worth recognizing well beyond dependency arrays specifically. Anywhere a correct answer requires multi-step reasoning that can plausibly be shortcut under generation pressure, the effective rule is usually one that forces the intermediate steps to happen explicitly and visibly, not one that simply states what the final correct answer should look like once arrived at.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for a team working across many sessions
&lt;/h2&gt;

&lt;p&gt;For a solo developer, inconsistent dependency arrays produce bugs that get discovered and fixed over time, unevenly, as each one happens to surface. For a team, the problem compounds differently, because multiple developers are generating effects independently, each one working from the same underlying tendency to shortcut the trace under pressure, and nobody has visibility into how the last three effects someone else wrote actually handled the same category of decision.&lt;/p&gt;

&lt;p&gt;A team without this rule accumulates a distribution of dependency array quality that mirrors individual session inconsistency, just multiplied across however many developers are contributing. Some effects will be correct. Some will be under-specified. Some will be over-specified. And because none of these failure modes throw an obvious error, the distribution tends to remain invisible until a specific bug forces someone to look closely at one particular effect, the same way my own stale data bug forced me to look closely enough to notice the second, opposite problem sitting right next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after adding the verification rule
&lt;/h2&gt;

&lt;p&gt;Since adding this more detailed key verification rule rather than just a blanket reminder about correct dependencies, both failure modes became noticeably less frequent, and interestingly, they became less frequent in somewhat different ways and to different degrees. The under-specification cases dropped the most substantially, since forcing an explicit enumeration of every referenced value catches the ones that were previously getting missed precisely because they were not obviously connected to the effect at a casual glance. The over-specification cases also dropped, though somewhat less dramatically, since the stability check in step four specifically targets the pattern of including an unstable object or function without addressing why it is unstable in the first place, which requires recognizing the underlying cause rather than just naming the symptom.&lt;/p&gt;

&lt;p&gt;The effects that remained genuinely complicated after the rule was in place were, importantly, complicated for legitimate structural reasons rather than because of an incomplete or shortcut dependency trace. A handful of effects did end up getting split into multiple smaller effects as a direct consequence of rule six, which turned out to be a reasonable and useful side effect of forcing more deliberate attention onto what each individual effect was actually responsible for doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;The dependency array problem looks like it should be simple to fix with a quick reminder, since there genuinely is one technically correct answer for any given effect, unlike patterns with multiple legitimate solutions. But the correct answer requires an effortful, multi-step trace through the surrounding code, and that trace is exactly the part that gets shortcut when generation is moving quickly or when the referenced values are not immediately, visibly obvious from the code sitting directly above the effect.&lt;/p&gt;

&lt;p&gt;A rule that simply states the correct answer does not fix this, because the AI already knows the correct answer when asked directly and still produces the inconsistent result during actual generation. A rule that forces the trace to happen as an explicit, named step does fix it, because it removes the option of shortcutting the analysis in favor of a quick pattern match. Look for other places in your codebase where the AI has one clearly correct answer readily available but still arrives at it inconsistently across different sessions, and consider whether the fix needs to force a specific reasoning step to happen rather than simply restate the destination it should already know how to reach.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project has correct-answer problems being silently shortcut during generation?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural decisions where the AI demonstrably has the right knowledge but skips the reasoning required to apply it consistently across every session.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Index as Key Is Not a Knowledge Problem. Your AI Already Knows the Rule. It Just Does Not Always Follow It.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Mon, 10 Aug 2026 15:50:50 +0000</pubDate>
      <link>https://dev.to/avery_code/index-as-key-is-not-a-knowledge-problem-your-ai-already-knows-the-rule-it-just-does-not-always-1359</link>
      <guid>https://dev.to/avery_code/index-as-key-is-not-a-knowledge-problem-your-ai-already-knows-the-rule-it-just-does-not-always-1359</guid>
      <description>&lt;p&gt;Ask any AI coding assistant directly whether using array index as a React key is a good idea, and it will tell you no. It will explain why. Reordering, insertion, and deletion of list items can cause React to misidentify which DOM node corresponds to which data, leading to state bugs and unnecessary re-renders. This is not obscure knowledge. It is one of the most commonly repeated pieces of React advice that exists, and every model has clearly seen it thousands of times during training.&lt;/p&gt;

&lt;p&gt;And yet, if you look through a codebase where the AI generated a meaningful portion of the list rendering, you will very likely find at least one instance of exactly this pattern. A map over an array, using the index as the key prop, sitting quietly in a component that otherwise looks perfectly reasonable.&lt;/p&gt;

&lt;p&gt;This is a strange thing to observe once you notice it. The AI is not confused about the rule. Ask it directly and it recites the correct answer immediately and confidently. But somewhere between knowing the rule in the abstract and applying it consistently during generation, something gets lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why knowing a rule and applying it are different things
&lt;/h2&gt;

&lt;p&gt;There is a meaningful difference between an AI model having encountered information during training and that information reliably surfacing during every relevant generation task.&lt;/p&gt;

&lt;p&gt;When you ask directly whether index as key is a good idea, you are prompting the model to retrieve and state a fact it has strong, well reinforced associations with. This is a different cognitive task than generating a list rendering component from scratch while simultaneously handling several other decisions about structure, naming, data shape, and styling.&lt;/p&gt;

&lt;p&gt;During active generation, the model is not running through a checklist of best practices for every line it writes. It is producing output token by token based on patterns, and in the moment of writing a map function, the path of least resistance is often exactly the pattern that gets flagged as wrong when examined afterward. Using item.id as a key requires the data to reliably have a stable id field, requires the model to correctly identify which field serves that purpose, and requires slightly more consideration than reaching for the index, which is always available and always simple.&lt;/p&gt;

&lt;p&gt;The rule is not forgotten. It is simply not the strongest pull during the specific moment of generating this specific line of code, especially when the data shape is ambiguous or when generation is happening quickly across a larger component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually shows up
&lt;/h2&gt;

&lt;p&gt;The pattern tends to appear in a few predictable situations rather than randomly across all list rendering.&lt;/p&gt;

&lt;p&gt;It shows up most often when the data being mapped does not have an obviously named unique identifier readily visible in the immediate context. A list of strings. A list of objects where the unique field is called something other than the expected id or key. A list assembled from a transformation where the original identifier got dropped somewhere in the pipeline.&lt;/p&gt;

&lt;p&gt;It shows up when the component is being generated as part of a larger request, where the list rendering is a small piece of a bigger component and gets less individual attention than it would if it were the sole focus of the prompt.&lt;/p&gt;

&lt;p&gt;It shows up in draft or placeholder-feeling code, static lists, dummy data, early stage components where correctness feels less pressing in the moment of generation, even though these components frequently end up shipping unchanged.&lt;/p&gt;

&lt;p&gt;None of these situations involve the AI forgetting the rule exists. They involve situations where correctly applying the rule requires slightly more work than the shortcut, and nothing in the generation process forces that extra work to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why asking for better prompts does not fully solve this
&lt;/h2&gt;

&lt;p&gt;A natural response is to just include the instruction in the prompt. Do not use index as key, use a proper unique identifier. This helps in the specific session where it is included, the same way any explicit instruction helps for that one request.&lt;/p&gt;

&lt;p&gt;But this does not solve the underlying pattern for the same reason that most single-session fixes do not solve recurring problems. The instruction has to be remembered and re-included every time list rendering comes up, across every developer using the AI on the project, across every session, indefinitely. Miss it once, in one prompt, in one session, and the shortcut reappears.&lt;/p&gt;

&lt;p&gt;This is different from the AI not knowing the rule. It is the rule not being present at the moment it needs to apply, because presence in a specific prompt is not the same as presence as a standing constraint that applies regardless of what that day's prompt happened to include.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually closes the gap
&lt;/h2&gt;

&lt;p&gt;The fix is not teaching the AI something it does not know. It already knows. The fix is removing the situation where knowing the rule and applying it under generation pressure can diverge.&lt;/p&gt;

&lt;p&gt;This means being specific about what counts as an acceptable key, not just stating the negative rule about what to avoid. A rule that says do not use index as key is less effective than a rule that specifies exactly what to use instead and what to do when the obvious identifier is missing.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key selection rule for list rendering:
1. Every list item requires a unique, stable identifier that persists across re-renders, insertions, and deletions of other items in the list.
2. If the data object has an id field, or any field that is guaranteed unique and stable, use that field directly as the key.
3. If no such field exists in the data, generate one during data transformation before the list reaches the rendering component, not as an inline fallback during the map call itself.
4. Index as key is acceptable only for lists that are static, never reordered, never filtered, and never have items inserted or removed during the component's lifetime. This is a narrow exception, not a default.
5. When uncertain whether a list qualifies for the exception in rule four, treat it as not qualifying and require a proper identifier.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This rule does something the general knowledge did not. It removes the ambiguity about what to do when the obvious id field is missing, which is exactly the situation where the shortcut tends to appear. Instead of the model having to decide in the moment whether this particular list is an exception, the rule states that uncertainty defaults to requiring a real identifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the specificity matters more than the reminder
&lt;/h2&gt;

&lt;p&gt;A rule that just says avoid index as keys functions as a reminder of something already known. A rule that specifies exactly what qualifies as acceptable, what does not, and what to do when the data does not have an obvious answer functions as a decision procedure.&lt;/p&gt;

&lt;p&gt;The difference matters because the actual failure point was never a lack of awareness. It was the absence of a clear procedure for the ambiguous cases, the ones where the ideal identifier is not immediately obvious and a decision has to be made quickly during generation. General awareness does not resolve ambiguity. A specific procedure does.&lt;/p&gt;

&lt;p&gt;This pattern generalizes beyond this one example. Anywhere the AI demonstrably knows a rule when asked directly but inconsistently applies it during generation, the fix is rarely restating the rule more forcefully. It is usually making the rule specific enough that it resolves the exact situations where the shortcut becomes tempting, rather than leaving those situations as judgment calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after adding the specific rule
&lt;/h2&gt;

&lt;p&gt;Since adding the more detailed key selection rule rather than just a blanket reminder, index as key stopped appearing in generated list rendering, including in the ambiguous cases that used to produce it most often. Lists without an obvious id field now consistently get a generated identifier during data transformation rather than falling back to the index inline.&lt;/p&gt;

&lt;p&gt;The interesting part is that this did not require teaching the AI anything new about why index as key causes problems. That knowledge was never missing. What changed was removing the specific decision point where the shortcut used to win by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Your AI is not confused about index as key. It has known the rule since before you started this project and it will tell you so if you ask it directly.&lt;/p&gt;

&lt;p&gt;The gap is not knowledge. It is the absence of a specific procedure for the exact situations where applying that knowledge requires more effort than skipping it. A general reminder does not close that gap. A specific rule about what counts as an acceptable identifier, and what to do when one is not obvious, does.&lt;/p&gt;

&lt;p&gt;Look for the other places in your project where the AI would give you the correct answer if asked directly but does not consistently apply it during generation. Those are not knowledge gaps. They are missing decision procedures, and they are usually easier to fix than they first appear.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project has knowledge gaps versus procedure gaps?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural decisions where a general rule is not enough and a specific procedure is missing.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>I Found Four Different Solutions to the Same Prop Drilling Problem in One Codebase. All Written by AI.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Wed, 05 Aug 2026 11:44:21 +0000</pubDate>
      <link>https://dev.to/avery_code/i-found-four-different-solutions-to-the-same-prop-drilling-problem-in-one-codebase-all-written-by-234a</link>
      <guid>https://dev.to/avery_code/i-found-four-different-solutions-to-the-same-prop-drilling-problem-in-one-codebase-all-written-by-234a</guid>
      <description>&lt;p&gt;I was looking for a specific piece of state logic and ended up finding something more interesting instead.&lt;/p&gt;

&lt;p&gt;Four components in the same project were passing data down through their children. Same underlying problem in every case. A piece of state needed several levels below where it originated. Classic prop drilling, the kind every React developer has run into at some point.&lt;/p&gt;

&lt;p&gt;What caught my attention was that each of the four instances solved it differently. One used React Context. One used a composition pattern, passing components as children instead of passing data as props. One just kept passing the props down through four levels without addressing it at all. One introduced a small state management library that was not used anywhere else in the project.&lt;/p&gt;

&lt;p&gt;Four different solutions. Same problem. Same codebase. No consistency between any of them.&lt;/p&gt;

&lt;p&gt;None of the four solutions was wrong exactly. Context is a legitimate answer to prop drilling. So is composition. So is a state library in the right circumstances. Even just passing props through multiple levels is defensible when the chain is short. But having all four approaches scattered across one project, with no indication of when each one applies, is not a technical problem. It is a standard problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this particular pattern reveals so much
&lt;/h2&gt;

&lt;p&gt;Prop drilling is a good lens for understanding how AI makes architectural decisions, because it does not have one correct answer. Unlike something with a clear right and wrong way to do it, prop drilling has several legitimate solutions, and the correct choice depends on context that is specific to your project rather than universal to React.&lt;/p&gt;

&lt;p&gt;Context is a good solution when the data is genuinely global to a subtree, changes infrequently, and does not need fine grained update control. Composition is a good solution when the components in between do not actually need the data themselves, they are just structurally in the way. A dedicated state management approach makes sense when the state has complex update logic or needs to be accessed from many unrelated parts of the tree. And sometimes just passing props through two or three levels is genuinely fine and does not need a more sophisticated solution at all.&lt;/p&gt;

&lt;p&gt;All four of these are correct in the right circumstances. The AI has been trained on enough React code to know all four patterns exist and roughly when each one tends to get used. What it does not have is a rule specific to your project about which one your project prefers, or under what threshold prop drilling stops being fine and starts needing a different pattern.&lt;/p&gt;

&lt;p&gt;Without that rule, every session makes an independent judgment call. And because the judgment call depends on subtle context, session to session variance is almost guaranteed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens during four separate sessions
&lt;/h2&gt;

&lt;p&gt;Session one. A component three levels deep needs a piece of user data. The AI, working from whatever context is visible in that session, decides Context makes sense here. It wraps the relevant subtree in a Context Provider and consumes it three levels down.&lt;/p&gt;

&lt;p&gt;Session two, weeks later, working on a different feature. A different component four levels deep needs a piece of data. The Context solution from session one is not visible in the current context window, or the AI does not connect this situation to that one. It decides composition is cleaner here, restructures the component tree so the data-needing component receives its content as a child prop instead.&lt;/p&gt;

&lt;p&gt;Session three. Another instance of the same underlying problem. This time the component chain is shorter, only two levels, so the AI just passes the prop through directly without introducing any additional pattern. Reasonable, in isolation.&lt;/p&gt;

&lt;p&gt;Session four. A more complex case, several pieces of related state needing to reach multiple deeply nested components. The AI reaches for a small state management solution because the complexity seems to warrant it, even though nothing else in the project uses that approach.&lt;/p&gt;

&lt;p&gt;Look at each of these individually and every decision is locally reasonable. The AI is not confused or making mistakes. It is applying its general React knowledge to each specific situation as it encounters it, without any awareness of how the previous three situations were handled.&lt;/p&gt;

&lt;p&gt;The problem only becomes visible when you look at all four together, which is exactly the kind of visibility that individual sessions never have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why more context does not fix this specific problem
&lt;/h2&gt;

&lt;p&gt;This is a case where the context and rules distinction becomes very concrete. Even with full access to the codebase, more context does not solve the prop drilling inconsistency, because the codebase itself already contains four different answers.&lt;/p&gt;

&lt;p&gt;If the AI has access to all four existing solutions when it encounters a fifth instance of the pattern, it does not have a clear signal about which one to follow. It has four examples showing four different approaches. Averaging across them or picking whichever one seems most similar to the current situation does not produce consistency. It produces a fifth variation, or at best, a coin flip between the four that already exist.&lt;/p&gt;

&lt;p&gt;This is different from a situation where context genuinely helps, like naming conventions where the existing codebase mostly shows one consistent pattern and the AI can reasonably infer and follow it. Prop drilling in this project had no consistent pattern to infer from. The inconsistency itself is what makes context unable to resolve the problem.&lt;/p&gt;

&lt;p&gt;Only an explicit rule breaks the tie. Something that says, independent of what the existing code happens to show, this is the threshold and this is the pattern above that threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an actual rule looks like for this specific problem
&lt;/h2&gt;

&lt;p&gt;The rule does not need to ban three of the four approaches entirely. It needs to define when each one applies, so the decision stops being a fresh judgment call every session.&lt;/p&gt;

&lt;p&gt;Here is a rule that resolves the ambiguity for prop drilling specifically:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prop drilling resolution rule:
1. Props passed through one or two levels are acceptable without any additional pattern. Do not introduce Context or composition for chains this short.
2. Props passed through three or more levels, where the intermediate components do not use the data themselves, get restructured using composition. Pass the deeply nested content as children rather than threading the data as props.
3. Data that is genuinely needed by multiple unrelated components across a subtree, rather than just passed through structurally, uses Context. This applies when three or more sibling branches of the tree need independent access to the same data.
4. Complex state with multiple related pieces, frequent updates, or logic beyond simple value storage uses the project's established state management approach, not an ad hoc alternative introduced for this one case.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Four situations, four clear answers. Not because three of the four legitimate patterns are wrong, but because the rule specifies exactly when each one is the right one for this project. The AI is no longer choosing based on whatever seems locally reasonable. It is checking the rule and applying the answer that has already been decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern this reveals beyond prop drilling
&lt;/h2&gt;

&lt;p&gt;Prop drilling happens to be a clean example because it has several genuinely correct answers that depend on context. But the same dynamic applies to any React decision that has more than one legitimate solution.&lt;/p&gt;

&lt;p&gt;Conditional rendering has several valid approaches. Ternary expressions, short-circuit evaluation, early returns, separate component variants. All correct in different circumstances. Without a rule specifying when each applies, expect the same session-to-session variance that prop drilling showed.&lt;/p&gt;

&lt;p&gt;Data fetching patterns have several valid approaches depending on whether the data is needed on initial render, needs caching, needs revalidation, or is fetched in response to user interaction. Without a rule, expect a different pattern chosen based on whatever seems reasonable in each specific session.&lt;/p&gt;

&lt;p&gt;Form handling has several valid approaches, from fully controlled components to uncontrolled refs to third party form libraries. Same story.&lt;/p&gt;

&lt;p&gt;Any React decision with more than one textbook-correct answer is a decision that will drift unless a rule specifies which answer applies under which conditions in your specific project. This is a broader category than most developers realize when they first start writing rules, because the instinct is to write rules for things that are obviously wrong. The actual highest value rules are often for things that are not wrong, just inconsistent, because those are exactly the decisions where the AI has multiple correct options and no way to know which one you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed after writing the rule
&lt;/h2&gt;

&lt;p&gt;Going back through sessions after the prop drilling rule was in place, the pattern was noticeably different. New instances of the same underlying problem consistently resolved the same way, based on the threshold defined in the rule rather than whatever seemed reasonable in that specific session.&lt;/p&gt;

&lt;p&gt;More importantly, the decision stopped requiring active review. Before the rule existed, every new instance of prop drilling needed a moment of evaluation during code review, checking whether this particular solution made sense or whether it should have been handled differently given what the rest of the codebase does. After the rule existed, the review could simply check compliance with the defined threshold, which is a much faster and more mechanical check than evaluating whether an architectural judgment call was reasonable.&lt;/p&gt;

&lt;p&gt;The four existing inconsistent instances did not fix themselves. Rules apply going forward, not retroactively. But no new instances of the inconsistency have appeared since, which is the actual goal. Preventing new instances of the drift matters more than immediately correcting the ones that already accumulated, because the accumulated ones are a known, bounded cost while ongoing drift is unbounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt doesn't matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Prop drilling is not a hard problem. React developers have known how to solve it for years, and the AI knows all the standard solutions as well as any experienced developer would.&lt;/p&gt;

&lt;p&gt;The inconsistency does not come from the AI lacking knowledge about how to solve prop drilling. It comes from the AI having multiple correct options and no project-specific rule about which one applies when. That gap exists for prop drilling and it exists for every other React decision that has more than one legitimate answer.&lt;/p&gt;

&lt;p&gt;Find the decisions in your project with more than one correct solution. Write down which solution applies under which conditions. And stop letting each session make an independent, locally reasonable choice that turns out to be different from the choice made three sessions ago.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project has multiple valid patterns doing the same job differently?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural gaps where more than one correct solution exists and no rule specifies which one your project actually uses.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>You Think Senior Developers Prompt Better. They Just Stopped Hoping the AI Would Guess Right.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:00:45 +0000</pubDate>
      <link>https://dev.to/avery_code/you-think-senior-developers-prompt-better-they-just-stopped-hoping-the-ai-would-guess-right-5dno</link>
      <guid>https://dev.to/avery_code/you-think-senior-developers-prompt-better-they-just-stopped-hoping-the-ai-would-guess-right-5dno</guid>
      <description>&lt;p&gt;There is a common belief about why experienced developers get better results from AI. The belief is that they write better prompts. More precise instructions. Better context. A sharper sense of how to phrase a request so the model understands what is actually needed.&lt;/p&gt;

&lt;p&gt;This belief is not entirely wrong, but it misses the actual mechanism behind the difference. Watch a senior developer work with AI over an extended period and the pattern that emerges is not primarily about prompt quality. It is about what happens before the prompt gets written at all.&lt;/p&gt;

&lt;p&gt;Junior developers tend to approach each session as a fresh negotiation. Write a prompt, see what comes back, correct what is wrong, ask again if needed. This is a reasonable way to work when you do not yet know what tends to go wrong repeatedly. Every project feels new. Every session is its own isolated interaction.&lt;/p&gt;

&lt;p&gt;Senior developers, after enough repetitions of the same corrections, stop treating each session as a fresh negotiation. They notice the same categories of mistake appearing across projects and across time, and instead of continuing to correct them one at a time, they write the correction down once and stop hoping the AI will guess correctly going forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specific moment where the shift happens
&lt;/h2&gt;

&lt;p&gt;This shift does not happen because someone reads an article about AI rules and decides to try it. It happens because a developer accumulates enough frustration with the same correction to finally write it down.&lt;/p&gt;

&lt;p&gt;The pattern looks something like this. A developer notices that the AI keeps putting state in the wrong place. They correct it. Next session, same mistake, different component. They correct it again. This repeats for weeks or months, and at some point the developer has corrected this specific mistake so many times that continuing to correct it manually starts to feel absurd.&lt;/p&gt;

&lt;p&gt;That moment of absurdity is the actual trigger. Not a decision to become more disciplined about writing rules. A recognition that the correction has become so repetitive that writing it down once is obviously less effort than correcting it forever.&lt;/p&gt;

&lt;p&gt;Junior developers have not yet accumulated enough repetitions to reach that moment. Every mistake still feels somewhat novel because they have not been doing this long enough to recognize the pattern. Senior developers have usually made the same category of correction often enough, across enough different codebases, that the pattern becomes impossible to ignore.&lt;/p&gt;

&lt;p&gt;This is why the difference looks like experience from the outside. It correlates with experience because experience is what generates the repetition needed to notice the pattern. But the actual mechanism is not years of practice improving prompt-writing skill. It is enough repeated corrections to finally stop correcting manually and write the rule instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hoping the AI will guess right is the default state
&lt;/h2&gt;

&lt;p&gt;Every developer starts in the same place, whether they realize it or not. The default assumption when working with AI is that better communication will eventually produce better results. If the output is wrong, the natural response is to explain more clearly, add more context, phrase the request differently.&lt;/p&gt;

&lt;p&gt;This assumption is not unreasonable on its face. Communication genuinely does affect output quality in the moment. A clearer prompt does produce a more accurate response to that specific request.&lt;/p&gt;

&lt;p&gt;What this assumption misses is that clearer communication in the prompt does not persist to the next session. Every session starts from the same baseline regardless of how well the previous session was communicated. The clarity you achieved in one prompt does not carry forward. It has to be recreated every time, in every session, for every developer who works on the project.&lt;/p&gt;

&lt;p&gt;Hoping the AI will guess right, even with better prompts, means hoping that this specific session's communication will happen to land on the right decision. Across enough sessions, across enough developers, across enough different types of components and features, some percentage of those hopes will not pan out. And the corrections required to fix the ones that do not pan out accumulate exactly as fast as the sessions themselves.&lt;/p&gt;

&lt;p&gt;Senior developers do not have some special ability to communicate more clearly that eliminates this. They have simply stopped relying on communication clarity as the mechanism for consistency, because they have seen it fail to produce consistency often enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What replaces the hoping
&lt;/h2&gt;

&lt;p&gt;The alternative to hoping the AI guesses right is removing the guess entirely. This is the actual practice that experienced developers converge on, whether or not they frame it explicitly as writing rules.&lt;/p&gt;

&lt;p&gt;It looks like maintaining a running list of the corrections that keep recurring, and periodically converting the most frequent ones into explicit constraints that get provided before any session starts. Not instructions embedded in a single prompt, but standing context that applies to every session regardless of what that session's specific prompt says.&lt;/p&gt;

&lt;p&gt;The practical difference is significant. A prompt that says please keep components small is a hope. A rule that says components exceeding two hundred lines must be split before continuing, applied consistently across every session, is a removal of the guess. The AI is not being asked to interpret what small means in this context. It has an explicit threshold and an explicit action.&lt;/p&gt;

&lt;p&gt;Here is what this looks like as an actual practice, based on what tends to recur across most React codebases regardless of team or project:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Corrections that become rules once they recur enough:
1. State placement. After correcting this the same way enough times, the rule becomes explicit: any state used by more than one component moves to a dedicated hook, not scattered useState calls, and this applies before any component gets written, not as a fix afterward.
2. Prop drilling versus context. Once a developer has manually flagged this the same way enough times, the threshold gets written down explicitly instead of evaluated fresh every session: props passed through more than two levels get reconsidered for context or composition before continuing.
3. Naming inconsistency. After noticing the same concept named three different ways across a project one too many times, the domain vocabulary gets documented explicitly so the AI is never guessing at which word applies.
4. Error handling gaps. Once a developer has added the same try-catch pattern manually enough times, the requirement becomes explicit: every async operation handles its error state before the component is considered complete.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;None of these represent a fundamentally different skill from what junior developers have access to. They represent the accumulated result of noticing the same problem enough times to stop treating it as something to fix individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this looks like a skill gap but is not one
&lt;/h2&gt;

&lt;p&gt;The reason this gets attributed to seniority is that seniority is genuinely correlated with having encountered these patterns more often. A developer with five years of experience has had more opportunities to notice the same recurring mistake than a developer with six months of experience.&lt;/p&gt;

&lt;p&gt;But the actual capability being described is not a skill that takes years to develop. It is a decision to stop correcting the same thing repeatedly and write it down instead. A developer with six months of experience who notices this pattern early and starts writing rules will get the same benefit as a developer with five years who has been correcting manually the entire time without ever converting the corrections into explicit rules.&lt;/p&gt;

&lt;p&gt;This matters because it means the gap is closeable much faster than the skill narrative suggests. If the difference were genuinely about prompt-writing ability developed through years of practice, there would be no way to shortcut it. But because the actual difference is about whether recurring corrections get written down as rules, any developer can close the gap by deliberately tracking what they correct and converting the patterns into explicit constraints, without needing years of accumulated repetition first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes once the rules exist
&lt;/h2&gt;

&lt;p&gt;The most noticeable change is not in any single session. It is in the aggregate pattern across weeks and months. The same categories of correction stop appearing. Not because the AI got smarter, but because the decisions that used to be guessed at freshly every session are no longer being guessed at.&lt;/p&gt;

&lt;p&gt;This frees up attention for the things that genuinely do require judgment in each specific case. The product logic. The edge cases that are actually unique to this feature. The architectural decisions that are genuinely ambiguous rather than recurring patterns that should have been standardized long ago.&lt;/p&gt;

&lt;p&gt;Developers who reach this state describe the shift less as their prompts becoming better and more as their sessions becoming less exhausting. The mental overhead of continuously catching the same categories of mistake disappears, and what remains is the part of the work that actually benefits from careful thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;The difference between developers who spend their sessions correcting the same recurring mistakes and developers who barely touch their AI's output is not a difference in prompting skill that takes years to acquire.&lt;/p&gt;

&lt;p&gt;It is whether the recurring corrections got written down once as explicit rules, or whether they continue getting fixed manually every single session, indefinitely, because nobody stopped to notice the pattern and remove the guessing.&lt;/p&gt;

&lt;p&gt;Track what you correct. Look for the patterns. Write the rules down. And stop hoping that this session's prompt will happen to land on the right decision when you could simply remove the decision from being a guess in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find which recurring corrections in your React project should already be rules?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The categories of correction that keep appearing because the decision behind them was never made explicit.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Code Review Happens After Generation. Your Standard Needs to Happen Before It.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Fri, 24 Jul 2026 13:49:24 +0000</pubDate>
      <link>https://dev.to/avery_code/your-code-review-happens-after-generation-your-standard-needs-to-happen-before-it-1l9g</link>
      <guid>https://dev.to/avery_code/your-code-review-happens-after-generation-your-standard-needs-to-happen-before-it-1l9g</guid>
      <description>&lt;p&gt;There is an assumption buried in how most teams think about AI generated code. The assumption is that the review process will catch what needs catching. The AI generates, a human reviews, problems get flagged, corrections happen. The system works the way code review has always worked, just with an AI in the author role instead of a person.&lt;/p&gt;

&lt;p&gt;This assumption misses something fundamental about how the AI actually operates. It does not review its own output. It does not second guess its architectural decisions. It does not pause halfway through generating a component and ask whether this is the right approach for this specific project. It generates, based on whatever it can infer from context and whatever instructions it received, and then it stops.&lt;/p&gt;

&lt;p&gt;There is no internal reviewer inside the generation process. The only reviewer is the human who looks at the output afterward. And by the time that review happens, every decision has already been made.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generation actually looks like from the inside
&lt;/h2&gt;

&lt;p&gt;When the AI generates a component, it makes a sequence of decisions very quickly. Where should this state live. What should this be called. How should this be structured. What pattern applies here. Each decision happens based on the immediate context and whatever general knowledge the model has, and then the next decision happens on top of it.&lt;/p&gt;

&lt;p&gt;There is no step in this process where the AI stops and evaluates whether the decision it just made matches your project's standard. It does not have access to your standard unless you gave it explicit rules to follow. It has its training, the immediate context, and the prompt. That is what informs every decision it makes during generation.&lt;/p&gt;

&lt;p&gt;This is different from how a human developer works, even one who is moving quickly. A human developer carries an internalized standard that operates continuously during the work. They do not consciously think about it most of the time, but it is there, shaping decisions as they are made. A senior developer writing a component is applying years of accumulated judgment about what this specific codebase should look like, even without articulating any of it.&lt;/p&gt;

&lt;p&gt;The AI has no equivalent internal process. It has no accumulated judgment about your specific project. It has patterns from its training and whatever is visible in the current context. Without explicit rules, every decision during generation is made in the absence of the standard that a human would have been applying automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why review after generation is the wrong place to catch this
&lt;/h2&gt;

&lt;p&gt;Code review exists to catch what generation itself does not catch. This has always been true, going back before AI was part of the workflow. A human developer writes code, another human reviews it, and the review catches logic errors, missed edge cases, security issues, things the original developer did not think of or got wrong.&lt;/p&gt;

&lt;p&gt;This works because human authorship already has a standard embedded in the generation process. The review is catching mistakes on top of a foundation that mostly reflects the project's conventions. The reviewer is looking for what went wrong, not establishing what the baseline should have been.&lt;/p&gt;

&lt;p&gt;With AI generated code the review is doing something different, whether anyone intends it to or not. Because there is no standard embedded in the generation process unless rules were provided beforehand, the review becomes the place where the standard gets established after the fact. The reviewer is not just catching mistakes. They are retroactively defining what should have happened, one comment at a time, on code that has already been written based on the AI's best guess without that definition.&lt;/p&gt;

&lt;p&gt;This is expensive in a way that traditional code review is not. The code already exists. The developer already spent time on it. Correcting it after the fact costs more than it would have cost to define the standard before generation began. And because the standard was never written down, the same retroactive correction happens again in the next pull request, and the one after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reviewer that does not exist
&lt;/h2&gt;

&lt;p&gt;If you think about what would actually solve this problem, the answer is obvious once you say it out loud. You need something reviewing the decisions as they get made, before the output reaches a human reviewer. You need a standard that operates during generation, the way a senior developer's internalized judgment operates during their own writing process.&lt;/p&gt;

&lt;p&gt;The AI cannot provide this on its own. It does not have accumulated judgment about your project. It has no internal reviewer checking its work against your conventions because it has no access to your conventions unless you give them to it explicitly.&lt;/p&gt;

&lt;p&gt;This is what rules actually do. They are not documentation for humans to reference later. They are the internal reviewer the AI does not otherwise have. When a rule says state belongs in a dedicated hook, that rule is operating during generation, before the component exists, shaping the decision the same way a human developer's internalized standard would shape their own writing.&lt;/p&gt;

&lt;p&gt;The difference between a rule and a code review comment is the difference between prevention and correction. A code review comment happens after the mistake is already in the codebase, in a pull request, taking up review time. A rule happens before the mistake has a chance to exist. The generation itself follows the standard because the standard was present during generation, not absent from it and discovered afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like when the standard operates during generation
&lt;/h2&gt;

&lt;p&gt;Consider a specific decision that happens constantly during React development. Should this piece of logic live in the component or in a hook.&lt;/p&gt;

&lt;p&gt;Without a rule, the AI decides this based on whatever it infers from context. Sometimes it puts logic in the component because that is simpler for the specific case. Sometimes it extracts a hook because the context suggests that pattern. The decision is made fresh every time, based on immediate signals rather than a consistent standard.&lt;/p&gt;

&lt;p&gt;With a rule specifying exactly when logic must be extracted into a hook, the decision is no longer being made fresh. It is being checked against a standard that exists before the component is written. The AI does not have to infer what your project prefers because your project's preference has been stated explicitly and it is present during the generation process itself.&lt;/p&gt;

&lt;p&gt;Here is what a set of rules that operate during generation actually looks like, as opposed to comments that would otherwise appear during review:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules that replace after-the-fact review comments:
1. Any state that affects more than one render decision moves into a dedicated hook, not inline useState calls scattered through the component. This is checked during generation, not flagged during review.
2. Components receiving more than four props get evaluated for whether they are doing too much before the component is written, not after a reviewer counts the props and asks why.
3. Any function passed as a prop follows the handleX naming convention consistently, checked as the function is named, not corrected in a review comment asking for a rename.
4. Data fetching never happens directly inside a component body. This is enforced as the component is structured, not caught when a reviewer notices a useEffect doing something it should not be doing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each of these represents a decision point during generation where a rule can operate instead of a human catching the absence of that rule after the fact. The review still happens. It just has less retroactive standard-setting to do because the standard was already present when the code was written.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for the humans doing review
&lt;/h2&gt;

&lt;p&gt;This does not eliminate code review. It changes what code review is for.&lt;/p&gt;

&lt;p&gt;When rules operate during generation, the review process gets to focus on what it was originally designed for. Logic correctness. Edge cases. Whether the feature actually does what the product requires. The kinds of judgment calls that genuinely benefit from a second set of eyes, as opposed to structural and conventional decisions that should have been consistent from the start.&lt;/p&gt;

&lt;p&gt;Reviewers stop writing the same comments about component structure and state placement and naming conventions, not because they got more lenient, but because those categories of feedback are no longer necessary. The AI was not left to guess at those decisions during generation. The rules already determined them.&lt;/p&gt;

&lt;p&gt;The time this saves compounds. Every pull request that does not need retroactive standard correction is a pull request where the reviewer's attention goes to something that actually requires their judgment. Over enough pull requests this is a substantial shift in how review time gets spent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Your AI does not review its own output. It generates and stops. There is no internal process checking whether the decisions it just made match your project's standard, unless that standard was given to it explicitly before generation started.&lt;/p&gt;

&lt;p&gt;Your human reviewers are catching this after the fact, one comment at a time, on code that has already been written without the standard that should have shaped it. This is not a failure of your review process. It is a mismatch between where the standard needs to operate and where it is currently being applied.&lt;/p&gt;

&lt;p&gt;Write the rules. Give them to the AI before generation happens. And let your review process finally focus on what it was built for instead of retroactively establishing a standard that should have been there from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project needs rules operating during generation instead of corrections during review?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you identify exactly that. The structural gaps where your standard is currently being discovered after the fact instead of applied before generation.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>react</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Your AI Has More Context Than Ever. It Still Does Not Know Your Standard. Context and Rules Are Not the Same Thing.</title>
      <dc:creator>Avery</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:33:45 +0000</pubDate>
      <link>https://dev.to/avery_code/your-ai-has-more-context-than-ever-it-still-does-not-know-your-standard-context-and-rules-are-not-49l3</link>
      <guid>https://dev.to/avery_code/your-ai-has-more-context-than-ever-it-still-does-not-know-your-standard-context-and-rules-are-not-49l3</guid>
      <description>&lt;p&gt;The promise of more context is compelling.&lt;/p&gt;

&lt;p&gt;If the AI can see more of your codebase before it generates, it should produce output that fits better. It should pick up on the patterns. It should infer the conventions. It should understand from what already exists how new things should be built.&lt;/p&gt;

&lt;p&gt;This is the argument behind tools that give the AI access to your entire project. More files. More history. More awareness of what surrounds the code it is about to write. The assumption is that more context produces more consistent output.&lt;/p&gt;

&lt;p&gt;The assumption is partially right and fundamentally incomplete.&lt;/p&gt;

&lt;p&gt;More context does help the AI make better local decisions. It can see that you use TypeScript strictly. It can see that you have hooks in a certain folder structure. It can pick up on some of the surface-level patterns and extend them.&lt;/p&gt;

&lt;p&gt;What it cannot do is infer your standard from your codebase. Because your codebase is the result of your standard, not a definition of it. And there is a critical difference between a result and a definition that determines whether more context actually solves the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What context actually gives the AI
&lt;/h2&gt;

&lt;p&gt;Context gives the AI examples.&lt;/p&gt;

&lt;p&gt;It can see what components look like in your project. It can see where hooks tend to live. It can observe naming patterns in the files it has access to. It can make educated guesses about conventions based on what it reads.&lt;/p&gt;

&lt;p&gt;This is genuinely useful. A model with access to your entire project will make better guesses than one with access to only the current file. The local consistency improves. The obvious mismatches become less common.&lt;/p&gt;

&lt;p&gt;But guessing from examples is not the same as following rules. And the difference between the two becomes visible exactly in the situations where consistency matters most.&lt;/p&gt;

&lt;p&gt;When the AI is guessing from examples it can only infer what the examples show consistently. If your codebase has any variation — any place where two different approaches exist, any historical inconsistency, any feature where something was done differently — the AI has to choose between them. And it will choose based on which pattern is more common in its context window, or which one appears most recently, or which one the current prompt seems to suggest.&lt;/p&gt;

&lt;p&gt;That choice is a guess. A more informed guess than without context. But a guess.&lt;/p&gt;

&lt;p&gt;Rules are not guesses. A rule says this is always done this way. Not this is usually done this way based on what I can see. Always. The AI does not have to infer anything. The decision has already been made and communicated explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cases where context fails and rules succeed
&lt;/h2&gt;

&lt;p&gt;The difference between context and rules becomes most visible in specific situations that every project eventually faces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legacy inconsistency.&lt;/strong&gt; Every codebase older than a few months has places where things were done differently before the current standard was established. Those inconsistencies exist in the context the AI reads. When the AI infers patterns from context, it averages across all of them, including the old ones you no longer want. Rules specify the current standard, not the historical average.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New patterns.&lt;/strong&gt; When your team decides to adopt a new approach — a new way of structuring features, a new state management pattern, a new import convention — the context initially shows the old pattern more than the new one. The AI will continue generating the old pattern because it is what the examples show. A rule immediately applies the new standard regardless of what the historical examples look like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguous situations.&lt;/strong&gt; Many architectural decisions are genuinely ambiguous from context alone. Should this state be local or global? Should this logic be in a hook or a service? Should this component be split now or later? The context shows examples of both approaches in different situations. The AI has to guess which applies here. Rules eliminate the guessing by making the decision explicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-session consistency.&lt;/strong&gt; Context changes every session based on which files are open, which parts of the codebase are visible, what the prompt focuses on. The AI's inferences change with the context. Rules are constant. They apply regardless of which files happen to be in context this session.&lt;/p&gt;

&lt;p&gt;These are not edge cases. They are the situations that produce the inconsistency that more context was supposed to fix. And more context does not fix them because the problem is not the quantity of examples. It is the absence of explicit decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why developers believe context is the answer
&lt;/h2&gt;

&lt;p&gt;The context argument is intuitive because it mirrors how human developers learn.&lt;/p&gt;

&lt;p&gt;A new developer joins a project and gets better by seeing more of the codebase. The more code they read, the better they understand the patterns. The more context they have, the more consistently they code.&lt;/p&gt;

&lt;p&gt;So it feels natural to apply the same logic to AI. Give it more context and it will code more consistently, just like a developer who has seen more of the project.&lt;/p&gt;

&lt;p&gt;But there is a crucial difference. A human developer does not just see examples. They have conversations. They ask questions. They get explanations. They receive explicit feedback in code reviews. They absorb not just what the codebase looks like but why it looks that way and what it should look like when they add to it.&lt;/p&gt;

&lt;p&gt;The AI only gets the examples. It does not get the conversations. It does not get the explanations. It does not get the code reviews that say "we stopped doing it that way six months ago, here is the current approach." It infers from what it can see, which is never the complete picture.&lt;/p&gt;

&lt;p&gt;Rules give the AI what conversations give a human developer. Not more examples. Explicit decisions. This is the standard. This is always the answer. This is what you do here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What rules give the AI that context cannot
&lt;/h2&gt;

&lt;p&gt;Rules are decisions made once and communicated explicitly.&lt;/p&gt;

&lt;p&gt;They do not require inference. They do not depend on the consistency of the existing codebase. They do not change based on which files happen to be in the context window. They apply every session regardless of what examples are visible.&lt;/p&gt;

&lt;p&gt;This is what makes them different from context in a way that matters practically.&lt;/p&gt;

&lt;p&gt;Consider the state placement decision. A project with more context might show the AI that state sometimes lives in components and sometimes in hooks. The AI infers that both are acceptable and makes a session-by-session judgment about which applies. Inconsistency.&lt;/p&gt;

&lt;p&gt;A rule says state belongs in a dedicated hook within the feature. Always. The AI does not have to infer anything. The decision has been made. Every session produces the same answer regardless of what the context shows.&lt;/p&gt;

&lt;p&gt;Here is what that looks like across a set of decisions that context cannot reliably determine:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules that context cannot replace:
1. State scope is decided before any code is written. Local state stays local. Shared state moves to a dedicated hook. Global state only when two independent features require it. Context shows examples of all three. The rule specifies which applies when.
2. The domain language is defined explicitly. Customer means this. User means that. Order means this. Cart means that. Never these alternatives. Context shows whatever words have been used. The rule defines which words should be used.
3. The current architectural pattern applies to new code even when the existing codebase shows the old pattern. Context averages across history. The rule specifies the present standard.
4. Import paths go through feature index files. Always. Not sometimes, not when it seems appropriate. Context shows some direct imports from the pre-refactor period. The rule says those are not the standard.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These rules cannot be inferred from context with the reliability that makes them useful. They have to be stated explicitly. Context is useful for many things. Replacing explicit decisions is not one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right relationship between context and rules
&lt;/h2&gt;

&lt;p&gt;Context and rules are not competing approaches. They work together and each does something the other cannot.&lt;/p&gt;

&lt;p&gt;Context helps the AI understand what already exists. It helps with local consistency. It helps the AI extend existing patterns when those patterns are clear and consistent. It is genuinely valuable and more context is generally better than less.&lt;/p&gt;

&lt;p&gt;Rules tell the AI what should exist. They define the standard that new code must meet regardless of what the existing code shows. They are the explicit decisions that remove ambiguity from the situations where context produces inconsistent guesses.&lt;/p&gt;

&lt;p&gt;A project with good context and good rules gets both benefits. The AI understands the existing codebase and follows a defined standard when adding to it. The output is consistent not because the AI made good guesses but because the decisions were made before the AI started guessing.&lt;/p&gt;

&lt;p&gt;A project with good context but no rules gets the benefit of better local guesses. The output is more consistent than without context. But it is still inconsistent in the ways that matter because the AI is still guessing at the decisions that rules would have made explicit.&lt;/p&gt;

&lt;p&gt;More context is not the answer to inconsistent AI output. It is a partial improvement on top of a missing foundation. The foundation is rules. And no amount of context replaces the need to define what the standard actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt does not matter. The rules do.
&lt;/h2&gt;

&lt;p&gt;Your AI has more context than ever. It can see your folder structure, your existing components, your patterns and naming and architectural decisions.&lt;/p&gt;

&lt;p&gt;And it still does not know your standard. Because your standard is not in the examples. It is in the decisions behind the examples. And decisions have to be stated explicitly to be followed consistently.&lt;/p&gt;

&lt;p&gt;Give your AI the context it needs to understand what exists. Give it the rules it needs to know what should exist. And stop expecting more examples to do the work that only explicit decisions can do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Want to find where your React project is relying on context where it needs rules?
&lt;/h2&gt;

&lt;p&gt;I built a free 24 point checklist that helps you find exactly that. The structural gaps where your AI is inferring instead of following and the inconsistency that results.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-clean-code-checklist?utm_source=devto" rel="noopener noreferrer"&gt;Get the React AI Clean Code Checklist — free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://averylabs.gumroad.com/l/avery-code-react-ai-coding-system-pro?utm_source=devto" rel="noopener noreferrer"&gt;Avery Code React AI Engineering System&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
