React Development Standards & Best Practices
Code Quality & Maintainability
-
Type Safety
- Use TypeScript for all new components
- Avoid
any
type (enabletsconfig.json
noImplicitAny
) - Define types/interfaces in the same file as components when possible
-
Security
- Never hardcode sensitive keys/credentials
- Use environment variables (
.env
files) with proper.gitignore
- Validate all external inputs and API responses
-
Component Design
- Single Responsibility Principle (one component = one purpose)
- Keep component size < 300 lines (split larger components)
- Prefer functional components with hooks
- Memoize expensive computations with
useMemo
Component Implementation
-
Props Management
// Good interface ButtonProps { variant: 'primary' | 'secondary'; onClick: () => void; } const Button = ({ variant, onClick }: ButtonProps) => (...) // Bad (types separate) type ButtonProps = any;
-
State Management
- Prefer Zustand for global state management
- Use React Context or Zustand only for cross-component state within a feature
- Avoid prop drilling through component composition
- Keep form state and UI state localized when possible
// Zustand store example import { create } from "zustand"; interface AuthStore { user: User | null; setUser: (user: User) => void; logout: () => void; } const useAuthStore = create<AuthStore>((set) => ({ user: null, setUser: (user) => set({ user }), logout: () => set({ user: null }), })); // Context usage example const ThemeContext = create<"light" | "dark">("light"); export const useTheme = () => useContext(ThemeContext);
-
API Handling
- Use React Query as primary solution
- Fallback to RTK Query if using Redux
- Axios for simple cases with interceptors
- Cache management through query client only
// Preferred stack const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes }, }, }); // Axios instance with interceptors const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, }); api.interceptors.request.use((config) => { const token = localStorage.getItem("access_token"); if (token) config.headers.Authorization = `Bearer ${token}`; return config; });
// React Query example import { useQuery } from "@tanstack/react-query"; const fetchUser = async (id: string) => { const { data } = await api.get(`/users/${id}`); return data; }; export const useUser = (id: string) => { return useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, retry: 2, }); }; // Axios interceptor example api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { window.location.href = "/login"; } return Promise.reject(error); } );
-
Query Invalidation
// React Query cache invalidation const mutation = useMutation({ mutationFn: updateUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users"] }); }, }); // Optimistic updates useMutation({ mutationFn: updateUser, onMutate: async (newUser) => { await queryClient.cancelQueries({ queryKey: ["user", newUser.id] }); const previousUser = queryClient.getQueryData(["user", newUser.id]); queryClient.setQueryData(["user", newUser.id], newUser); return { previousUser }; }, onError: (err, newUser, context) => { queryClient.setQueryData(["user", newUser.id], context.previousUser); }, });
Styling
-
CSS Practices
- Use TailwindCSS as primary styling solution
- Material UI only for existing implementations
- Mobile-first responsive design approach
- Use CSS variables for dynamic theming
// Good tailwind usage <div className="flex flex-col md:flex-row gap-4 p-4">{children}</div>
-
Tailwind Best Practices
// Responsive card component const Card = ({ children }: { children: ReactNode }) => ( <div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 md:p-6 lg:p-8 transition-colors duration-300"> {children} </div> ); // Loading skeleton const SkeletonLoader = () => ( <div className="animate-pulse space-y-4"> <div className="h-4 bg-gray-200 rounded w-3/4"></div> <div className="h-4 bg-gray-200 rounded"></div> </div> );
Testing & Quality
-
Unit Testing
- 100% test coverage for utility/helper functions
- Core component functionality must be tested
- Use React Testing Library with Vitest
- Test user flows, not implementation details
Performance
-
Memoization & Optimization
- Profile with React DevTools
- Avoid unnecessary re-renders
- Virtualize long lists
// Memoized component const ExpensiveList = React.memo(({ items }: { items: Item[] }) => ( <ul> {items.map((item) => ( <ListItem key={item.id} item={item} /> ))} </ul> )); // useCallback example const SearchForm = () => { const [query, setQuery] = useState(""); const handleSearch = useCallback( debounce((searchTerm: string) => { searchAPI(searchTerm); }, 300), [] ); return <input onChange={(e) => handleSearch(e.target.value)} />; };
- Implement image CDN with auto-format/compression
- Lazy load non-critical assets
- Cache static assets with service workers
Code Structure
- File Organization
- Follow atomic design pattern
- Group by feature, not type
- Keep tests colocated (
Component.test.tsx
)
Dependency Management
- Package Rules
- Audit dependencies monthly
- Prefer React ecosystem libraries
- Limit global state management to one solution
Documentation
-
Project Documentation
- Architecture decision records (ADR)
- Tech stack justification in
/docs/tech-stack.md
- Setup guide with PNPM commands
- State management flow diagrams
-
[Optional] Component Documentation ```tsx /**
- @component AsyncButton
- @description Button with loading state for async operations
- @prop {ReactNode} children - Button content
- @prop {() => Promise
} onClick - Async click handler - @prop {boolean} isLoading - Loading state
- @example
- <AsyncButton
- onClick={handleSubmit}
- isLoading={isSubmitting}
-
- Submit
- </AsyncButton> */ export const AsyncButton = ({ children, onClick, isLoading }) => ( <button onClick={onClick} disabled={isLoading} className={
px-4 py-2 ${isLoading ? "bg-gray-400" : "bg-blue-600"}
}{isLoading ? <Spinner /> : children} </button> ); ```
Pipeline Enforcement
-
Required checks:
- Vitest (min 80% branch coverage) - ESLint (typescript-react-preset) - TypeScript strict mode - Bundle size limit (150kb gzip) - Lighthouse CI (performance >= 90) - Dependency audit (pnpm audit)
-
Vitest Configuration
// vitest.config.ts export default defineConfig({ test: { environment: "jsdom", coverage: { provider: "v8", reporter: ["text", "json", "html"], thresholds: { lines: 85, functions: 85, branches: 85, statements: 85, }, }, }, });
Development Workflow
-
Package Manager
- Prefer PNPM (commit
pnpm-lock.yaml
) - Yarn acceptable for legacy projects
- Prefer PNPM (commit
-
Responsive Requirements
- Loading skeletons for async content
- Optimistic UI updates
- Graceful error boundaries
Image Optimization
- Image Optimization
// Image component with optimizations <img src="/image.jpg" alt="Description" loading="lazy" className="w-full h-auto" srcSet="/image-320w.jpg 320w, /image-640w.jpg 640w, /image-1024w.jpg 1024w" sizes="(max-width: 640px) 100vw, 50vw" />
Responsive Requirements
-
Breakpoints
// Tailwind default breakpoints sm: 640px // Small md: 768px // Medium lg: 1024px // Large xl: 1280px // Extra large
Component Testing Strategy
// Component test example
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
describe("Button component", () => {
it("renders with correct variant", () => {
render(<Button variant="primary">Click</Button>);
expect(screen.getByRole("button")).toHaveClass("bg-primary");
});
});
// Hook test example
import { renderHook, waitFor } from "@testing-library/react";
import { useUser } from "./userHooks";
describe("useUser hook", () => {
it("fetches user data", async () => {
const { result } = renderHook(() => useUser("123"));
await waitFor(() => expect(result.current.isSuccess).toBe(true));
});
});
Component Composition
// Avoid prop drilling
const UserProfile = ({ user }) => (
<Card>
<Avatar user={user} />
</Card>
);
// Better composition
const UserProfile = ({ children }) => <Card>{children}</Card>;
// Usage
<UserProfile>
<Avatar user={user} />
</UserProfile>;
Accessibility
-
WCAG Compliance
- All interactive elements must have keyboard navigation
- ARIA labels for icon buttons
- Color contrast ratio ≥ 4.5:1
- Semantic HTML structure
// Good accessibility practice <button aria-label="Close modal" className="p-2 hover:bg-gray-100" onClick={onClose} > <XIcon className="w-5 h-5" /> </button> // Loading state accessibility <div role="alert" aria-live="polite" className={isLoading ? 'block' : 'hidden'} > Loading results... </div>
Lighthouse Requirements
Performance: 90
Accessibility: 95
Best Practices: 95
SEO: 90
PWA: 70
Additional Best Practices
SOLID Principles
- Single Responsibility Principle (SRP): Each function should have a single responsibility. Do not mix business logic, UI manipulation, and event handling in the same function.
- Open/Closed Principle (OCP): Components should be extendable without modifying their base code. Example: A
Form
should not have theButton
embedded; instead, theButton
should be an independent component.
Avoid Repetition (DRY)
- If you detect repeated code (components, functions, or hooks), extract it into a custom hook, reusable component, or independent utility.
- Example:
// Bad: Repeated logic in multiple components useEffect(() => { fetch('/api/data') .then(res => res.json()) .then(setData); }, []); // Better: Extracting to a reusable hook const useFetchData = (url: string) => { const [data, setData] = useState(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(setData); }, [url]); return data; }; const Component = () => { const data = useFetchData('/api/data'); };
Descriptive Variable Names
- Variable names should be clear and descriptive.
Correct Examples:
- Use
isLoading
instead ofloading
- Use
userList
instead ofusers
- Use
modalRef
if usinguseRef
- Use
handleSubmit
for a submit function instead ofsubmit
State Updates Should Use setState(prevState => {...})
- Avoid modifying state directly without considering the previous state.
Incorrect Example:
setCounter(counter + 1);
Correct Example:
setCounter(prevCounter => prevCounter + 1);