Stop Shipping Demo Code: Hardening AI-Generated React for Production
AI-generated React code passes the demo and fails the on-call.
We’ve all seen the prototype. It renders instantly, the data flows smoothly, and the UI looks polished. Then you hand it to a senior engineer—or worse, deploy it to production—and the “vibe-coded” prototype collapses under the weight of real-world edge cases. The API returns a 500, the network drops, or the user navigates away mid-request. The application either hangs indefinitely or presents a blank white screen.
This is the fundamental gap between AI-generated code and production-ready software. AI models are trained on tutorials and examples that prioritize success paths. They default to “demo-grade” patterns: raw useEffect hooks for data fetching, generic try/catch blocks that swallow errors, and minimal state management. These patterns work in isolation but fail catastrophically in complex, distributed systems.
The 2025 State of React survey highlights a growing tension: while React usage is surging due to AI code generation, developers are increasingly frustrated by ecosystem complexity and build tooling integration [1]. The tooling is there, but the mental model for hardening AI output is not. We need to move beyond writing code that works in the happy path and start building resilient pipelines that handle failure gracefully.
The AI-Generated Code Trap
The primary failure mode of AI-generated React applications is the “blank screen” effect. When an AI model writes a data-fetching component, it typically uses a raw fetch call inside a useEffect hook. If the network request fails or the server returns an error status, the component simply does not update. The user sees nothing. There is no feedback, no loading state, and no error message.
This is not just a UX issue; it is a debugging nightmare. Without proper error boundaries or logging, these failures are silent. The developer has no idea why the data didn’t load until a user reports it.
Furthermore, AI models often over-engineer state management. They might introduce Redux or Zustand for simple local state, adding bundle size and complexity where it isn’t needed. The reality of modern React development is that for many applications, remote state handled by TanStack Query and local state managed via URL parameters or Context is sufficient. Adding heavy state management libraries increases the surface area for bugs and makes the codebase harder to maintain [5].
We must reject the idea that AI code is “good enough” because it compiles. Compilation is the floor, not the ceiling. Production readiness requires explicit handling of failure modes, structured logging, and recoverable UI states.
Error Surfaces: From White Screens to Recoverable UI
To harden an AI-generated React app, we must first define where errors occur and how they are handled. The standard approach of wrapping individual components in try/catch is insufficient. Errors in React are asynchronous and can propagate up the component tree in unpredictable ways.
Route-Level Error Boundaries
The first line of defense is the route-level error boundary. This boundary captures the React component stack and sends it to an error tracking service like Sentry. By implementing a RouteErrorBoundary, we ensure that if a top-level route fails, the user is presented with a fallback UI rather than a blank screen [2].
This boundary should not just catch the error; it should capture the context. What was the user doing? What was the component stack? This information is critical for debugging. Without it, we are guessing.
Feature-Level Boundaries and Recovery
Beyond route-level boundaries, we need feature-level boundaries for in-context recovery. Not all errors require a full page reload or a generic error message. Some errors are transient and can be recovered from automatically.
This is where the RecoverableErrorBoundary pattern comes in. This component tracks retry counts and maximum retries. If an error occurs, it attempts to recover automatically before failing over to a user-facing error message [3]. For example, if a data fetch fails due to a temporary network glitch, the boundary can retry the request. If it fails again, it might show a “Retry” button. If it fails a third time, it displays a permanent error state.
The decision to auto-retry versus showing an error depends on the nature of the failure. Network errors are often transient and worth retrying. Data validation errors are not. We must distinguish between these cases to avoid infinite retry loops or frustrating user experiences.
Browser Logs: Telemetry for the On-Call Engineer
Console logs are for development. Production logging requires structure, severity levels, and context. AI-generated code rarely includes production-grade logging because it is verbose and often omitted in tutorials.
Structured Logging Levels
Production logging should distinguish between three levels: INFO, WARN, and ERROR.
- INFO logs capture business events, such as a user completing a purchase or updating a profile. These are useful for analytics and auditing.
- WARN logs capture recoverable issues, such as cache misses or deprecated API usage. These indicate potential problems that do not break the application but should be monitored.
- ERROR logs capture unrecoverable failures, such as unhandled exceptions or failed API calls. These require immediate attention.
By structuring logs this way, we reduce noise and improve on-call response times. We can filter out INFO logs during an incident and focus on ERROR logs [3].
Injecting Context
Logs are useless without context. We must inject user context, such as userId, componentStack, and requestId, into every log entry. This allows us to trace a specific user’s journey through the application and identify where things went wrong.
Recoverable State: Simplifying the Stack
One of the most significant improvements we can make to an AI-generated React app is simplifying its state management. AI models often default to complex state management solutions because they are shown them in tutorials. However, for many applications, this is overkill.
Replacing useEffect with TanStack Query
The most common anti-pattern in AI-generated React code is using useEffect for data fetching. This pattern is error-prone and difficult to manage. Instead, we should use TanStack Query (formerly React Query).
TanStack Query handles caching, retrying, and background updates automatically. It replaces raw useEffect fetches with a declarative API that is easier to reason about. It also provides built-in support for aborting requests, which is crucial for handling AI streaming errors [4].
Minimal State Management
We do not need Redux, Zustand, or Jotai for every application. For many modern React apps, remote state handled by TanStack Query and local state managed via URL or Context is sufficient. This reduces bundle size and complexity [5].
If we do need client-side state, we should use it sparingly. URL state is often the best choice for shared state between components, as it is persistent and shareable. Local state should be used only for transient UI state, such as form inputs or modal visibility.
Handling AI Streaming Errors
AI features require a three-layer architecture to avoid tangled API calls. This includes typed patterns for streaming chat and semantic search that handle aborts and parse errors gracefully [4]. We must use AbortController to cancel requests when the user navigates away or when a new request is made. We must also handle parse errors, which are common when dealing with streaming AI responses.
The Pre-Flight Audit: Six Dimensions of Production Readiness
To ensure our AI-generated React apps are production-ready, we need a rigorous pre-flight audit. This isn’t a checklist to tick off; it’s a framework for identifying gaps in our code before they become incidents.
- Data: Use TanStack Query or RTK Query over raw
fetch. Ensure proper caching and retry logic. - Errors: Implement route and feature error boundaries. Integrate Sentry for error tracking.
- Types: Enforce strict TypeScript with
noUncheckedIndexedAccess. This prevents common runtime errors caused by undefined values. - Performance: Conduct memoisation audits. Use
React.memoanduseMemowhere appropriate to prevent unnecessary re-renders. - Accessibility: Ensure focus management and semantic landmarks. AI-generated code often lacks proper ARIA attributes and keyboard navigation.
- Observability: Integrate Sentry and PostHog at critical routes, such as authentication and payment. Monitor performance and user behavior.
This audit covers the most critical aspects of production readiness. By auditing our code against these dimensions, we can ensure that our AI-generated React apps are robust, maintainable, and user-friendly.
Sources and further reading
Find more practical writing from the RodyTech archive.
RodyTech publishes practical writing on AI systems, infrastructure, and software that teams can actually ship. Use the archive paths below to keep reading by topic or browse the full library.
- Browse the full archive by publication date and topic
- Hands-on notes from real builds, deployments, and ops work
- Category paths for AI, infrastructure, developer tools, and security
No comments yet