6.4 KiB
6.4 KiB
name, description
| name | description |
|---|---|
| react-frontend | Use when building React components — hooks, state management, TanStack Query, React Router, performance patterns, and testing. Triggers from .tsx/.jsx files and framework file patterns. |
React Frontend Best Practices
Component Patterns
Composition over Inheritance
// ✅ Prefer composition
function Layout({ sidebar, children }: { sidebar: ReactNode; children: ReactNode }) {
return <div className="layout">{sidebar}<main>{children}</main></div>;
}
// ❌ Avoid inheritance patterns in React
Container/Presentational Separation
// Container: manages state, data fetching, business logic
function UserProfileContainer() {
const { data: user } = useUserQuery(userId);
const { mutate: update } = useUpdateUserMutation();
return <UserProfile user={user!} onUpdate={update} />;
}
// Presentational: pure rendering, props-only
function UserProfile({ user, onUpdate }: UserProfileProps) {
return <div>{user.name} <button onClick={onUpdate}>Edit</button></div>;
}
Custom Hooks for Logic Extraction
// Extract reusable logic into custom hooks
function useUserPermissions(userId: string) {
const { data: user } = useUserQuery(userId);
return useMemo(() => ({
isAdmin: user?.role === 'admin',
canEdit: user?.role === 'admin' || user?.role === 'editor',
canDelete: user?.role === 'admin',
}), [user]);
}
Hooks Rules
- Only call hooks at the top level — not in conditions, loops, or callbacks.
- Only call hooks from React functions — component or custom hook.
- Deps array matches reality — include all values used inside.
useMemofor expensive computations.useCallbackfor stable references.useEffectis for synchronization, not lifecycle. If you can compute from state, do it.
// ❌ Unnecessary effect
const [fullName, setFullName] = useState('');
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// ✅ Derived state
const fullName = `${first} ${last}`;
Data Fetching (TanStack Query)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Query
function useUserQuery(id: string) {
return useQuery({
queryKey: ['users', id],
queryFn: () => api.users.get({ params: { id } }),
staleTime: 30_000, // 30s before refetch
gcTime: 5 * 60_000, // 5min cache
});
}
// Mutation with optimistic update
function useUpdateUserMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateUserInput) => api.users.update({ body: data }),
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users', newUser.id] });
const previous = queryClient.getQueryData(['users', newUser.id]);
queryClient.setQueryData(['users', newUser.id], newUser);
return { previous };
},
onError: (_, __, context) => {
queryClient.setQueryData(['users', context.previous.id], context.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
});
}
Rules:
staleTimefor read-through caching. Default 0 = refetch on mount.gcTimefor garbage collection of unused data.onMutate/onError/onSettledfor optimistic updates.- Queries over custom fetch + useEffect in all cases.
State Management Selection
| Need | Solution |
|---|---|
| Server state | TanStack Query |
| URL state | React Router / TanStack Router |
| Form state | React Hook Form + Zod |
| Client state (global) | Zustand or Context |
| Client state (local) | useState / useReducer |
| Component communication | Props / lifting state up |
// Zustand — lightweight, no boilerplate
import { create } from 'zustand';
interface UIStore {
sidebarOpen: boolean;
toggleSidebar: () => void;
}
const useUIStore = create<UIStore>((set) => ({
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
Routing (TanStack Router / React Router)
// TanStack Router — type-safe, modern
const router = createRouter({
routeTree: rootRoute.addChildren([
indexRoute,
usersRoute.addChildren([userRoute, userProfileRoute]),
]),
});
- File-based routing (Next.js App Router, Vite/Router) for simpler projects.
- Type-safe routers (TanStack Router) for larger apps.
- Lazy load route components —
React.lazy(() => import('./routes/Dashboard')).
Performance
- Virtual lists —
@tanstack/react-virtualfor 100+ items. - React.memo sparingly — only for components that re-render often with same props.
useMemofor expensive calculations — not for every value.- Code splitting — per route, per heavy component.
- Bundle analysis —
vite-bundle-visualizerto find bloat.
Testing (Vitest + Testing Library)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('renders user name', () => {
render(<UserProfile user={{ name: 'Alice' }} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
});
it('calls onUpdate when edit clicked', async () => {
const onUpdate = vi.fn();
render(<UserProfile user={{ name: 'Alice' }} onUpdate={onUpdate} />);
await userEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(onUpdate).toHaveBeenCalledTimes(1);
});
});
- Testing Library for user-centric tests. Never test implementation details.
- userEvent over
fireEvent— simulates real user interactions. - Component-level tests for behavior, not storybook-style visual tests here.
CSS / Styling
- Tailwind CSS for utility-first styling. Consistent, fast, small.
- CSS Modules when you need scoped component styles.
- CSS-in-JS (styled-components, emotion) — only for dynamic theming. Prefer Tailwind.
Anti-patterns
- ❌
useEffectfor data fetching — use TanStack Query - ❌ Prop drilling beyond 3 levels — compose or context
- ❌
useStatefor derived data — compute from existing state - ❌ Direct DOM manipulation — use React refs
- ❌
anyin component props — always type props - ❌ Large component files — split by responsibility
- ❌
indexas key — breaks reconciliation on reorder - ❌
useEffectwithout deps — runs every render