Files
seedit/docs/react-guide.md
plebeius e16d0537a4 add docs
2026-01-04 12:32:30 +01:00

813 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# React guide
A lot of AI-generated React code is heavily biased toward using useState and useEffect, often in verbose or naive ways. Thats 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 models 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. Heres 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 doesnt 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 cant:
- 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.
Heres 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 (
<>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
</ul>
</>
);
}
```
✅ 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 (
<>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
</ul>
</>
);
}
```
## 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 (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
```
✅ 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 <div>Loading...</div>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
```
## 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 <div>{results.length} results</div>;
}
```
✅ 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 <div>{results.length} results</div>;
}
```
Better yet? Dont 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 <div>{count}</div>;
}
function ComponentB() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
}
```
✅ 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 <div>{count}</div>;
}
function ComponentB() {
const count = useCounter();
return <div>{count}</div>;
}
```
## 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 (
<div>
{/* Complex conditional rendering based on multiple state variables */}
{isLoading && <LoadingSpinner />}
{isError && <ErrorMessage />}
{!isLoading && !isError && !orderPlaced && (
<>
<CartItems items={cart} onAddItem={addToCart} />
<PaymentForm onSubmit={submitPayment} />
{paymentDetails && <OrderButton onClick={placeOrder} />}
</>
)}
{orderPlaced && <OrderConfirmation />}
</div>
);
}
```
✅ 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 (
<div>
{error && <ErrorMessage message={error} />}
{state === 'browsing' && (
<CartItems
items={cart}
onAddItem={addToCart}
onCheckout={goToCheckout}
/>
)}
{state === 'paymentEntry' && (
<PaymentForm
onSubmit={submitPayment}
onBack={goBack}
/>
)}
{state === 'processingPayment' && (
<LoadingSpinner message="Processing payment..." />
)}
{state === 'confirmOrder' && (
<OrderSummary
cart={cart}
paymentDetails={paymentDetails}
onConfirm={placeOrder}
onBack={goBack}
/>
)}
{state === 'processingOrder' && (
<LoadingSpinner message="Placing your order..." />
)}
{state === 'orderComplete' && (
<OrderConfirmation orderDetails={{ cart, paymentDetails }} />
)}
</div>
);
}
```
### 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 (
<div>
{results.map(item => <p key={item.id}>{item.title}</p>)}
</div>
);
}
```
✅ 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 (
<div>
{isPending && <div>Loading...</div>}
{results.map(item => <p key={item.id}>{item.title}</p>)}
</div>
);
}
```
## 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 (
<div>
{route === '/' && <Home />}
{route === '/about' && <About />}
<button onClick={() => {
window.history.pushState({}, '', '/about');
setRoute('/about');
}}>
About
</button>
</div>
);
}
```
✅ 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 (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
```
## 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 <div>{data}</div>;
}
function App() {
return <MyComponent data={null} />;
}
```
✅ 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 <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
function MyComponent({ data }) {
if (!data) {
throw new Error("Data not found");
}
return <div>{data}</div>;
}
function App() {
return (
<ErrorBoundary>
<MyComponent data={null} />
</ErrorBoundary>
);
}
```
## 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 (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
);
}
```
✅ 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<typeof contactSchema>;
export function ContactForm() {
// 3. Hook up RHF with the zodResolver
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ContactFormData>({
resolver: zodResolver(contactSchema),
});
const onSubmit = (data: ContactFormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('name')}
placeholder="Name"
aria-invalid={!!errors.name}
/>
{errors.name && <p role="alert">{errors.name.message}</p>}
</div>
<div>
<input
{...register('email')}
placeholder="Email"
aria-invalid={!!errors.email}
/>
{errors.email && <p role="alert">{errors.email.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
```
## 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 (
<div>
<HeavyComponent />
</div>
);
}
```
✅ 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 (
<Suspense fallback={<div>Loading Component...</div>}>
<HeavyComponent />
</Suspense>
);
}
```
## 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(<MyComponent />);
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(<MyComponent />);
// 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 <div onClick={() => console.log('Clicked!')}>Click me</div>;
}
```
✅ Good (Semantic button with ARIA attributes and theming via CSS variables):
```jsx
// Good example: Accessible button component that utilizes theming.
function ThemedButton() {
return (
<button
aria-label="Click me"
onClick={() => console.log('Clicked!')}
style={{ padding: '8px 16px', backgroundColor: 'var(--primary-color)', color: '#fff' }}
>
Click me
</button>
);
}
/* 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, propdrilling | Central store (Zustand / Redux Toolkit) or colocated custom hooks |
| **Data Fetching** | `useEffect`+`fetch`, manual loading/error juggling | React Query / SWR with caching, retries, stalewhilerevalidate |
| **Side Effects** | Effects without cleanup → race conditions, leaks | AbortController or query library handles cancellation & retries |
| **Logic Reuse** | Copypasted hooks and state in every component | Extract crosscutting logic into custom hooks |
| **Workflow / State Machine** | Booleanflag soup, impossible states | Deterministic statechart in a single store (Zustand / XState) |
| **Concurrent UI Updates** | Blocking synchronous work inside effects | `useTransition` / `startTransition` for lowpriority updates |
| **Routing & Navigation** | DIY history manipulation, inline route state | React Router v6 (or Next.js builtins) with lazyloaded routes |
| **Error Handling** | No error boundary → fullapp crashes | Toplevel `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 ondemand loading |
| **Testing Strategy** | Brittle tests keyed to DOM structure | Usercentric tests with React Testing Library & jestdom |
| **Accessibility & Theming** | Nonsemantic elements, missing ARIA, inline colors | Semantic HTML, ARIA labels, CSS variables / theme provider |