DEV Community

Cover image for I Counted Three Different Loading Patterns Across Five Components. All Generated by the Same AI.
Avery
Avery

Posted on

I Counted Three Different Loading Patterns Across Five Components. All Generated by the Same AI.

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.

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.

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.

Why loading states specifically tend to fragment this way

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.

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.

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.

What determines which pattern gets chosen in any given session

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.

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.

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.

Why this particular inconsistency matters more than it initially seems

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.

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.

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.

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.

What a rule for this actually needs to specify

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.

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.
Enter fullscreen mode Exit fullscreen mode

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.

Why removing an option entirely is sometimes the right move

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.

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.

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.

What changed after the rule went into effect

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.

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.

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.

The prompt does not matter. The rules do.

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.

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.


Want to find where your React project has small UI decisions that were never actually standardized?

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.

Get the React AI Clean Code Checklist — free

Avery Code React AI Engineering System

Top comments (2)

Collapse
 
topstar_ai profile image
Luis Cruz

It's interesting to see how AI can generate different loading patterns based on context, highlighting the nuances of user experience design. Your insights into when to use spinners versus skeletons are particularly valuable; establishing a consistent pattern across components could enhance the overall user experience. If you're considering standardizing the loading states in your project, I’d be happy to discuss how I can assist with this implementation. Have you thought about creating a design system for these patterns?

Collapse
 
avery_code profile image
Avery

Yes, that is essentially what the rule in the article does, a small design system specifically for loading states rather than leaving it as an open decision each time. The key part was removing plain text as an option entirely rather than trying to define when it would be acceptable, since it never really was a deliberate choice to begin with. Once the skeleton versus spinner decision was tied to whether the content shape is predictable, it stopped being something that needed to be decided fresh in each new component.