# React guide A lot of AI-generated React code is heavily biased toward using useState and useEffect, often in verbose or naive ways. That’s because these hooks are: 1. Explicit, making them easy for LLMs to pattern-match. 2. Safe defaults, requiring less architectural decision-making. 3. Well-documented, so the model’s training data is oversaturated with examples. But modern, well-architected React apps rarely rely on raw useState/useEffect beyond simple UI state or lifecycle quirks. Here’s a breakdown of what serious devs use instead—and why this is the real moat for human engineers. Alternatives to useState and useEffect 1. State Machines / Statecharts (e.g. Zustand) - Why: Replaces ad-hoc local state management with deterministic, declarative logic. - Advantage: Less bugs, easier to test, clearer state transitions. - AI gap: LLMs struggle to model state transitions declaratively. LLM Agent must overcome this. 2. React Query / SWR for Data Fetching - Why: Avoids useEffect-based data fetching and loading/error state juggling. - Advantage: Caching, stale-while-revalidate, retries, pagination—all handled out of the box. - AI gap: AI often uses useEffect + fetch and forgets cleanup, race conditions, or caching. 3. Global State with Zustand, Jotai, Recoil, or Redux Toolkit - Why: Local useState doesn’t scale for shared or persistent state. - Advantage: Better performance, devtools support, persistence. - AI gap: Defaults to prop-drilling or bloated context APIs. 4. Custom Hooks and Composition - Why: Reuse logic cleanly without bloating components or copy-pasting useEffect. - Advantage: Separation of concerns, encapsulation. - AI gap: Often fails to factor logic out, keeping everything in one component. ## Why This Matters AI can mimic patterns, but it can’t: - Architect a system for long-term maintainability. - Predict runtime performance tradeoffs. - Refactor spaghetti effects into custom hooks or state machines. - Decide when state should be colocated vs globalized. - Avoid footguns like stale closures, unnecessary re-renders, or race conditions. In short: AI is great at writing code, but bad at engineering software. Takeaway - Use fewer useEffect and useState hooks. - Learn to design systems, not just code components. - Reach for declarative, composable patterns. - Understand caching, reactivity, and scalability beyond local state. Here’s a side-by-side breakdown of common AI-generated React patterns (bad) vs what experienced engineers write (good) across 4 key areas: state management, data fetching, side effects, and logic reuse. ## 1. State Management ❌ Bad (AI-style useState) ```typescript function TodoApp() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(''); const addTodo = () => { setTodos([...todos, { text: input }]); setInput(''); }; return ( <> setInput(e.target.value)} /> ); } ``` ✅ Good (Zustand store, state extracted) ```typescript import { create } from 'zustand'; const useTodoStore = create((set) => ({ todos: [], input: '', addTodo: () => set((state) => ({ todos: [...state.todos, { text: state.input }], input: '', })), setInput: (val) => set({ input: val }), })); function TodoApp() { const { todos, input, setInput, addTodo } = useTodoStore(); return ( <> setInput(e.target.value)} /> ); } ``` ## 2. Data Fetching ❌ Bad (AI-style useEffect) ```typescript function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers); }, []); return ( ); } ``` ✅ Good (React Query) ```typescript import { useQuery } from '@tanstack/react-query'; function UserList() { const { data: users = [], isLoading } = useQuery({ queryKey: ['users'], queryFn: () => fetch('/api/users').then(res => res.json()), }); if (isLoading) return
Loading...
; return ( ); } ``` ## 3. Side Effects ❌ Bad (naive effect with race condition) ```typescript function Search({ term }) { const [results, setResults] = useState([]); useEffect(() => { fetch(`/api/search?q=${term}`) .then(res => res.json()) .then(setResults); }, [term]); return
{results.length} results
; } ``` ✅ Good (abort previous fetch with AbortController) ```typescript function Search({ term }) { const [results, setResults] = useState([]); useEffect(() => { const controller = new AbortController(); fetch(`/api/search?q=${term}`, { signal: controller.signal }) .then(res => res.json()) .then(setResults) .catch((e) => { if (e.name !== 'AbortError') console.error(e); }); return () => controller.abort(); }, [term]); return
{results.length} results
; } ``` Better yet? Don’t use useEffect at all. Use React Query with term as a key. ## 4. Logic Reuse ❌ Bad (repeated logic inline) ```typescript function ComponentA() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); return
{count}
; } function ComponentB() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => setCount(c => c + 1), 1000); return () => clearInterval(id); }, []); return
{count}
; } ``` ✅ Good (custom hook) ```typescript function useCounter(intervalMs = 1000) { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => setCount(c => c + 1), intervalMs); return () => clearInterval(id); }, [intervalMs]); return count; } function ComponentA() { const count = useCounter(); return
{count}
; } function ComponentB() { const count = useCounter(); return
{count}
; } ``` ## 5. State Machines with Zustand ❌ Bad (AI-style complex useState with boolean flags) ```typescript function OrderProcess() { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [isSuccess, setIsSuccess] = useState(false); const [cart, setCart] = useState([]); const [paymentDetails, setPaymentDetails] = useState(null); const [orderPlaced, setOrderPlaced] = useState(false); const addToCart = (item) => { setCart([...cart, item]); }; const submitPayment = async (details) => { setIsLoading(true); setIsError(false); try { await processPayment(details); setPaymentDetails(details); setIsSuccess(true); setIsLoading(false); } catch (error) { setIsError(true); setIsLoading(false); } }; const placeOrder = async () => { setIsLoading(true); setIsError(false); try { await submitOrder(cart, paymentDetails); setOrderPlaced(true); setIsSuccess(true); setIsLoading(false); } catch (error) { setIsError(true); setIsLoading(false); } }; return (
{/* Complex conditional rendering based on multiple state variables */} {isLoading && } {isError && } {!isLoading && !isError && !orderPlaced && ( <> {paymentDetails && } )} {orderPlaced && }
); } ``` ✅ Good (Zustand state machine) ```typescript import { create } from 'zustand'; // Create a Zustand store with state machine pattern const useOrderStore = create((set, get) => ({ // State machine's current state state: 'browsing', // Context/data cart: [], paymentDetails: null, error: null, // Actions that transition between states addToCart: (item) => set((state) => ({ cart: [...state.cart, item] })), goToCheckout: () => set({ state: 'paymentEntry' }), goBack: () => { const { state: currentState } = get(); if (currentState === 'paymentEntry') { set({ state: 'browsing' }); } else if (currentState === 'confirmOrder') { set({ state: 'paymentEntry' }); } }, submitPayment: async (details) => { set({ state: 'processingPayment', error: null }); try { const result = await processPayment(details); set({ state: 'confirmOrder', paymentDetails: result }); } catch (error) { set({ state: 'paymentEntry', error: error.message }); } }, placeOrder: async () => { const { cart, paymentDetails } = get(); set({ state: 'processingOrder', error: null }); try { await submitOrder(cart, paymentDetails); set({ state: 'orderComplete' }); } catch (error) { set({ state: 'confirmOrder', error: error.message }); } } })); function OrderProcess() { const { state, cart, paymentDetails, error, addToCart, goToCheckout, goBack, submitPayment, placeOrder } = useOrderStore(); // Render UI based on current state return (
{error && } {state === 'browsing' && ( )} {state === 'paymentEntry' && ( )} {state === 'processingPayment' && ( )} {state === 'confirmOrder' && ( )} {state === 'processingOrder' && ( )} {state === 'orderComplete' && ( )}
); } ``` ### Key Benefits of the Zustand State Machine Approach: 1. **Explicit states**: The system can only be in one well-defined state at a time 2. **Centralized logic**: All state transitions are defined in one place 3. **Predictable transitions**: State changes follow clear patterns 4. **Simplified UI logic**: Components just render based on the current state 5. **Async handling**: Asynchronous operations are contained within the state transitions 6. **Shared state**: Any component can access the store without prop drilling 7. **Improved debugging**: The current state is always clear and predictable 8. **No impossible states**: Unlike boolean flags that could create invalid combinations 9. **Simpler API**: Compared to XState, Zustand offers a more approachable API for many developers 10. **Smaller bundle size**: Zustand is significantly smaller than XState This approach gives most of the benefits of formal state machines while maintaining the simplicity and familiarity of React state management. It's a pragmatic middle ground that's often easier to adopt in existing projects. ## 6. Concurrent UI Techniques ❌ Bad (Blocking UI updates without concurrency): ```jsx // Bad example: Synchronously fetching data in useEffect can cause UI jank. function SearchResults({ query }) { const [results, setResults] = React.useState([]); React.useEffect(() => { fetch(`/api/search?q=${query}`) .then(res => res.json()) .then(data => setResults(data)); }, [query]); return (
{results.map(item =>

{item.title}

)}
); } ``` ✅ Good (Using useTransition for non-blocking updates): ```jsx // Good example: Leveraging useTransition to mark state updates as low-priority. function SearchResults({ query }) { const [results, setResults] = React.useState([]); const [isPending, startTransition] = React.useTransition(); React.useEffect(() => { startTransition(() => { fetch(`/api/search?q=${query}`) .then(res => res.json()) .then(data => setResults(data)); }); }, [query, startTransition]); return (
{isPending &&
Loading...
} {results.map(item =>

{item.title}

)}
); } ``` ## 7. Routing and Navigation (Not applicable with Next.js apps) ❌ Bad (Manual routing without a dedicated library): ```jsx // Bad example: Handling routing manually using state and window history. function App() { const [route, setRoute] = React.useState(window.location.pathname); React.useEffect(() => { const onPopState = () => setRoute(window.location.pathname); window.addEventListener('popstate', onPopState); return () => window.removeEventListener('popstate', onPopState); }, []); return (
{route === '/' && } {route === '/about' && }
); } ``` ✅ Good (Using React Router v6 with lazy-loaded routes): ```jsx // Good example: Utilizing React Router to manage routes and lazy load components. import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import React, { lazy, Suspense } from 'react'; const Home = lazy(() => import('./Home')); const About = lazy(() => import('./About')); function App() { return ( Loading...}> } /> } /> ); } ``` ## 8. Error Handling ❌ Bad (No error boundary; crashes on errors): ```jsx // Bad example: A component that throws errors without an error boundary. function MyComponent({ data }) { if (!data) { throw new Error("Data not found"); } return
{data}
; } function App() { return ; } ``` ✅ Good (Wrapping components with an Error Boundary): ```jsx // Good example: Using an ErrorBoundary component to catch and handle errors gracefully. import React from 'react'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error("Error caught in boundary:", error, errorInfo); } render() { if (this.state.hasError) { return

Something went wrong.

; } return this.props.children; } } function MyComponent({ data }) { if (!data) { throw new Error("Data not found"); } return
{data}
; } function App() { return ( ); } ``` ## 9. Form Management ❌ Bad (Manual state handling with no validation): ```jsx // Bad example: Uncontrolled form without validation; error handling is omitted. function ContactForm() { const [formData, setFormData] = React.useState({ name: '', email: '' }); const handleSubmit = (e) => { e.preventDefault(); console.log(formData); }; return (
setFormData({ ...formData, name: e.target.value })} placeholder="Name" /> setFormData({ ...formData, email: e.target.value })} placeholder="Email" />
); } ``` ✅ Good (Using React Hook Form with Zod for schema validation): ```jsx import React from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // 1. Define your schema with Zod const contactSchema = z.object({ name: z.string().nonempty('Name is required'), email: z .string() .nonempty('Email is required') .email('Invalid email'), }); // 2. Infer the TypeScript type of your form data type ContactFormData = z.infer; export function ContactForm() { // 3. Hook up RHF with the zodResolver const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(contactSchema), }); const onSubmit = (data: ContactFormData) => { console.log(data); }; return (
{errors.name &&

{errors.name.message}

}
{errors.email &&

{errors.email.message}

}
); } ``` ## 10. Code Splitting and Lazy Loading ❌ Bad (Eagerly importing heavy components, increasing the initial bundle size): ```jsx // Bad example: Directly importing a large component causing a heavier bundle. import HeavyComponent from './HeavyComponent'; function App() { return (
); } ``` ✅ Good (Lazy loading heavy components with React.lazy and Suspense): ```jsx // Good example: Dynamically importing components to reduce initial load time. import React, { lazy, Suspense } from 'react'; const HeavyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( Loading Component...}> ); } ``` ## 11. Testing Strategies ❌ Bad (Testing implementation details, resulting in brittle tests): ```jsx // Bad example: Relying on internal DOM structure which may change. import { render } from '@testing-library/react'; import MyComponent from './MyComponent'; test('MyComponent renders data', () => { const { container } = render(); expect(container.querySelector('.data')).toBeDefined(); }); ``` ✅ Good (Focusing on user-centric behaviors using React Testing Library): ```jsx // Good example: Testing component behavior by simulating user interactions. import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import MyComponent from './MyComponent'; test('MyComponent displays loaded data after user action', async () => { render(); // Check for an initial loading state expect(screen.getByText(/Loading/i)).toBeInTheDocument(); // Simulate user action that triggers data loading const button = screen.getByRole('button', { name: /load data/i }); userEvent.click(button); // Wait and assert that data is displayed expect(await screen.findByText(/Data loaded/i)).toBeInTheDocument(); }); ``` ## 12. Accessibility & Theming ❌ Bad (Non-semantic elements and missing ARIA labels): ```jsx // Bad example: Using a div as a clickable element without semantics or accessibility. function ThemedButton() { return
console.log('Clicked!')}>Click me
; } ``` ✅ Good (Semantic button with ARIA attributes and theming via CSS variables): ```jsx // Good example: Accessible button component that utilizes theming. function ThemedButton() { return ( ); } /* CSS (in a separate file or styled-components): :root { --primary-color: #007bff; } */ ``` # Summary Table | Concern | Bad AI Pattern | Solid Engineering Practice | |---------|---------------|----------------------------| | **State Management** | `useState` everywhere, prop‑drilling | Central store (Zustand / Redux Toolkit) or colocated custom hooks | | **Data Fetching** | `useEffect` + `fetch`, manual loading/error juggling | React Query / SWR with caching, retries, stale‑while‑revalidate | | **Side Effects** | Effects without cleanup → race conditions, leaks | AbortController or query library handles cancellation & retries | | **Logic Reuse** | Copy‑pasted hooks and state in every component | Extract cross‑cutting logic into custom hooks | | **Workflow / State Machine** | Boolean‑flag soup, impossible states | Deterministic statechart in a single store (Zustand / XState) | | **Concurrent UI Updates** | Blocking synchronous work inside effects | `useTransition` / `startTransition` for low‑priority updates | | **Routing & Navigation** | DIY history manipulation, inline route state | React Router v6 (or Next.js built‑ins) with lazy‑loaded routes | | **Error Handling** | No error boundary → full‑app crashes | Top‑level `ErrorBoundary` plus logging in `componentDidCatch` | | **Form Management** | Manual state, zero validation or feedback | React Hook Form + Yup/Zod schema for declarative validation | | **Code Splitting & Lazy Loading** | Eager imports ballooning bundle size | `React.lazy` + `Suspense` for on‑demand loading | | **Testing Strategy** | Brittle tests keyed to DOM structure | User‑centric tests with React Testing Library & jest‑dom | | **Accessibility & Theming** | Non‑semantic elements, missing ARIA, inline colors | Semantic HTML, ARIA labels, CSS variables / theme provider |