# React.js Code Examples

Reference examples for each rule area. Read the relevant section when generating code for that area.

---

## Component Rules

### Always use Functional Components

```tsx
// ✅ Good
const UserCard = ({ user }: UserCardProps) => {
  return <div>{user.name}</div>;
};

// ❌ Bad — class component
class UserCard extends React.Component {}
```

### Define Props with TypeScript interface

```tsx
// ✅ Good
interface UserCardProps {
  user: User;
  onDelete?: (id: number) => void;
  className?: string;
}

const UserCard = ({ user, onDelete, className }: UserCardProps) => {
  // ...
};

// ❌ Bad — any, or no type
const UserCard = ({ user }: any) => {};
const UserCard = (props) => {};
```

### Keep components focused and small

```tsx
// ✅ Good — logic extracted to hook
const UserList = () => {
  const { users, isLoading, error, deleteUser } = useUserList();

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage message={error.message} />;

  return (
    <ul>
      {users.map(user => (
        <UserCard key={user.id} user={user} onDelete={deleteUser} />
      ))}
    </ul>
  );
};

// ❌ Bad — logic mixed into component
const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/users')
      .then(r => r.json())
      .then(data => { setUsers(data); setLoading(false); });
  }, []);

  const handleDelete = async (id) => {
    await fetch(`/api/users/${id}`, { method: 'DELETE' });
    setUsers(prev => prev.filter(u => u.id !== id));
  };
  // ... long JSX
};
```

### Export convention

```tsx
// Named export for regular components
export const UserCard = ({ user }: UserCardProps) => { ... };

// Default export only for page-level components
export default function UserPage() { ... }
```

---

## Custom Hook Rules

```tsx
// ✅ Good
const useUserList = () => {
  const { data: users = [], isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: userApi.getAll,
  });

  const { mutate: deleteUser } = useMutation({
    mutationFn: userApi.delete,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
  });

  return { users, isLoading, error, deleteUser };
};

// Usage
const { users, isLoading, deleteUser } = useUserList();
```

---

## State Management Rules

### Server state → TanStack Query

```tsx
// Fetching
const { data, isLoading, error } = useQuery({
  queryKey: ['users', filters],   // include deps in key
  queryFn: () => userApi.getAll(filters),
  staleTime: 5 * 60 * 1000,      // 5 minutes
});

// Mutation
const { mutate, isPending } = useMutation({
  mutationFn: userApi.create,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['users'] });
    toast.success('User created');
  },
  onError: (error) => toast.error(error.message),
});
```

### Client/UI state → Zustand (or useState for local)

```tsx
// Local state — use useState
const [isOpen, setIsOpen] = useState(false);

// Shared UI state — use Zustand
const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,
  login: (user, token) => set({ user, token }),
  logout: () => set({ user: null, token: null }),
}));
```

---

## Form Rules

Use **React Hook Form + Zod** for all forms:

```tsx
// 1. Define schema with Zod
const createUserSchema = z.object({
  email: z.string().email('Invalid email'),
  fullName: z.string().min(2, 'Min 2 characters').max(100),
  password: z.string().min(8, 'Min 8 characters'),
});

type CreateUserForm = z.infer<typeof createUserSchema>;

// 2. Use in component
const CreateUserForm = () => {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<CreateUserForm>({
    resolver: zodResolver(createUserSchema),
  });

  const onSubmit = async (data: CreateUserForm) => {
    await createUser(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Saving...' : 'Create'}
      </button>
    </form>
  );
};
```

---

## API Layer Rules

One `axios` instance shared across the app with interceptors:

```tsx
// src/services/api.ts
const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
});

// Attach token automatically
api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().token;
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Handle 401 globally
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      useAuthStore.getState().logout();
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);
```

Feature API module:

```tsx
// src/features/users/api.ts
export const userApi = {
  getAll: (params?: UserListParams): Promise<User[]> =>
    api.get('/users', { params }).then(r => r.data),

  getById: (id: number): Promise<User> =>
    api.get(`/users/${id}`).then(r => r.data),

  create: (data: CreateUserRequest): Promise<User> =>
    api.post('/users', data).then(r => r.data),

  update: (id: number, data: UpdateUserRequest): Promise<User> =>
    api.put(`/users/${id}`, data).then(r => r.data),

  delete: (id: number): Promise<void> =>
    api.delete(`/users/${id}`).then(r => r.data),
};
```

---

## TypeScript Rules

```tsx
// ✅ Good
interface User {
  id: number;
  email: string;
  fullName: string;
  createdAt: string;
}

type UserStatus = 'active' | 'inactive' | 'banned';

const getUser = async (id: number): Promise<User> => {
  return api.get(`/users/${id}`).then(r => r.data);
};

// ❌ Bad
const getUser = async (id: any) => {
  return api.get(`/users/${id}`).then((r: any) => r.data);
};
```

---

## Styling Rules (Tailwind CSS)

```tsx
// ✅ Good — cn() for conditional classes
import { cn } from '\@/utils/cn';

const Button = ({ variant = 'primary', disabled, className, children }: ButtonProps) => (
  <button
    className={cn(
      'px-4 py-2 rounded font-medium transition-colors',
      variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
      variant === 'outline' && 'border border-gray-300 hover:bg-gray-50',
      disabled && 'opacity-50 cursor-not-allowed',
      className
    )}
    disabled={disabled}
  >
    {children}
  </button>
);

// ❌ Bad — inline style
<button style={{ backgroundColor: 'blue', padding: '8px 16px' }}>
```

---

## Performance Rules

```tsx
// Lazy routes
const UserPage = React.lazy(() => import('./pages/UserPage'));

<Suspense fallback={<PageSkeleton />}>
  <UserPage />
</Suspense>

// useMemo for expensive computation
const sortedUsers = useMemo(
  () => [...users].sort((a, b) => a.fullName.localeCompare(b.fullName)),
  [users]
);
```

---

## Error Handling Rules

```tsx
// Error boundary at route level
<ErrorBoundary fallback={<ErrorPage />}>
  <Routes>
    <Route path="/users" element={<UserPage />} />
  </Routes>
</ErrorBoundary>

// Async error handling
const { mutate } = useMutation({
  mutationFn: userApi.create,
  onSuccess: () => toast.success('User created successfully'),
  onError: (error: AxiosError<ApiError>) =>
    toast.error(error.response?.data?.message ?? 'Something went wrong'),
});
```

---

## Testing Rules

### Unit/Component tests with React Testing Library

```tsx
// Test behavior, not implementation
describe('UserCard', () => {
  it('should display user name and email', () => {
    const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };
    render(<UserCard user={user} />);

    expect(screen.getByText('Test User')).toBeInTheDocument();
    expect(screen.getByText('test\@example.com')).toBeInTheDocument();
  });

  it('should call onDelete with user id when delete button clicked', async () => {
    const onDelete = vi.fn();
    const user: User = { id: 1, email: 'test\@example.com', fullName: 'Test User' };

    render(<UserCard user={user} onDelete={onDelete} />);
    await userEvent.click(screen.getByRole('button', { name: /delete/i }));

    expect(onDelete).toHaveBeenCalledWith(1);
  });
});
```

### Hook tests

```tsx
import { renderHook, waitFor } from '\@testing-library/react';

it('should fetch users', async () => {
  const { result } = renderHook(() => useUserList(), { wrapper: QueryWrapper });

  await waitFor(() => expect(result.current.isLoading).toBe(false));

  expect(result.current.users).toHaveLength(2);
});
```
