react Best Practices
This guide outlines the non-negotiable standards for building React applications within our team. Adherence ensures predictable behavior, simplifies debugging, and enables future optimizations.
1. Core React Principles: Purity & Rules of Hooks
Components and Hooks must be pure. They should always return the same output given the same inputs (props, state, context) and not cause side effects during rendering. Obey the Rules of Hooks without exception.
❌ BAD: Impure component / Side effect in render
function ProductList({ products }) {
// ❌ Modifies external data during render
products.sort((a, b) => a.name.localeCompare(b.name));
return (/* ... */);
}
function MyComponent() {
// ❌ Hook called conditionally
if (Math.random() > 0.5) {
const [count, setCount] = useState(0);
}
return (/* ... */);
}
✅ GOOD: Pure component / Correct Hook usage
import { useMemo, useState } from 'react';
function ProductList({ products }) {
// ✅ Sort data immutably or memoize if expensive
const sortedProducts = useMemo(() =>
[...products].sort((a, b) => a.name.localeCompare(b.name)),
[products]
);
return (/* ... */);
}
function MyComponent() {
// ✅ Hooks always at the top level
const [count, setCount] = useState(0);
// ... conditional logic after hooks
return (/* ... */);
}
2. Code Organization & Naming
Organize code by feature using the bulletproof-react pattern. Use TypeScript (.tsx) for all components.
- One Component Per File: Except for small, pure, stateless components closely related to a parent.
- Naming:
- Components:
PascalCase(e.g.,UserProfile.tsx) - Custom Hooks:
useprefix +PascalCase(e.g.,useAuth.ts) - Functions/Variables:
camelCase - CSS Classes:
kebab-case(via CSS Modules or utility classes)
- Components:
✅ GOOD: Feature-based structure
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.tsx
│ │ │ └── AuthButton.tsx
│ │ ├── hooks/
│ │ │ └── useAuth.ts
│ │ └── api/auth.ts
│ └── products/
│ ├── components/
│ │ ├── ProductCard.tsx
│ │ └── ProductList.tsx
│ └── hooks/useProducts.ts
├── components/ui/ // Reusable, generic UI components
│ ├── Button.tsx
│ └── Modal.tsx
└── App.tsx
3. Component Design & Patterns
Prioritize function components with Hooks. Separate concerns into "smart" (data/logic) and "dumb" (presentational) components.
❌ BAD: Class components / Mixed concerns
// ❌ Class component (avoid)
class UserProfile extends React.Component { /* ... */ }
// ❌ Component fetches data AND renders complex UI
function ProductPage() {
const [products, setProducts] = useState([]);
useEffect(() => { /* fetch products */ }, []);
return (/* complex product list UI */);
}
✅ GOOD: Function components / Separation of concerns
// ✅ Use function components with hooks
function UserProfile({ user }) { /* ... */ }
// ✅ Smart component (container) handles data fetching
function ProductListContainer() {
const { products, isLoading } = useProducts(); // Custom hook for data
if (isLoading) return <LoadingSpinner />;
return <ProductList products={products} />; // Renders dumb component
}
// ✅ Dumb component (presentational) focuses on UI
function ProductList({ products }) {
return (
<ul>
{products.map(product => <ProductCard key={product.id} product={product} />)}
</ul>
);
}
4. State Management
Start with local state (useState, useReducer). Lift state up when necessary. Use Context API for global state that rarely changes. For complex global state, use dedicated libraries (e.g., Zustand, Jotai, Redux Toolkit). Avoid prop drilling.
❌ BAD: Prop drilling
function Grandparent() {
const [theme, setTheme] = useState('dark');
return <Parent theme={theme} setTheme={setTheme} />;
}
function Parent({ theme, setTheme }) {
return <Child theme={theme} setTheme={setTheme} />;
}
function Child({ theme, setTheme }) {
return <Button => setTheme('light')}>Toggle Theme</Button>;
}
✅ GOOD: Context API for global state
import { createContext, useContext, useState, ReactNode } from 'react';
type Theme = 'light' | 'dark';
type ThemeContextType = { theme: Theme; toggleTheme: () => void };
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('dark');
const toggleTheme = () => setTheme(prev => (prev === 'dark' ? 'light' : 'dark'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// Usage:
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return <button theme: {theme}</button>;
}
5. Performance & Optimization
Optimize only when profiling indicates a bottleneck. Use React.memo, useCallback, useMemo judiciously.
❌ BAD: Premature optimization / Incorrect memoization
// ❌ Memoizing a component that re-renders frequently or has no expensive props
const MyButton = React.memo(({ onClick, children }) => <button
// ❌ Callback with missing dependency, causing stale closure
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log(count); // ❌ 'count' is stale if not in dependency array
}, []);
return <Child />;
}
✅ GOOD: Targeted optimization / Correct dependencies
import React, { useCallback, useMemo, useState } from 'react';
// ✅ Memoize only if component is expensive AND props are stable
const ExpensiveList = React.memo(({ items }) => {
console.log('Rendering ExpensiveList');
return (/* ... render many items ... */);
});
function Parent() {
const [count, setCount] = useState(0);
// ✅ Callback with correct dependencies
const handleClick = useCallback(() => {
setCount(prev => prev + 1); // Use functional update to avoid 'count' in deps
}, []);
// ✅ Memoize expensive calculations
const computedValue = useMemo(() => {
// ... heavy computation based on count ...
return count * 2;
}, [count]);
return (
<>
<ExpensiveList items={[{ id: 1, name: 'Item 1' }]} /> {/* Example usage */}
<Child />
<p>Count: {count}, Computed: {computedValue}</p>
</>
);
}
// Child component that receives the memoized callback
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Rendering Child');
return <button
});
6. Common Pitfalls
- Never mutate props or state directly. Always create new objects/arrays.
- Never call component functions directly. Use JSX.
- Ensure
useEffectcleanup functions are always provided for subscriptions or timers. - Correct
useEffectdependency arrays are critical to avoid infinite loops or stale closures.
❌ BAD: Direct mutation / Calling component as function
function MyComponent({ items }) {
// ❌ Mutating props directly
items.push('new item');
const [data, setData] = useState({ value: 1 });
// ❌ Mutating state directly
data.value = 2;
setData(data);
// ❌ Calling component as a function
return MyOtherComponent();
}
✅ GOOD: Immutable updates / JSX usage
function MyComponent({ items }) {
const [data, setData] = useState({ value: 1 });
// ✅ Create a new array for updates
const updatedItems = [...items, 'new item'];
// ✅ Create a new object for state updates
setData(prevData => ({ ...prevData, value: 2 }));
// ✅ Use JSX for components
return <MyOtherComponent />;
}
7. Accessibility (A11y) & Testing
Build for accessibility from the start. Test components as a user would.
- Semantic HTML: Use native HTML elements (
<button>,<input>,<a>) whenever possible. - ARIA Attributes: Use
aria-*attributes only when semantic HTML is insufficient. - Keyboard Navigation: Ensure all interactive elements are keyboard accessible and have proper focus management.
- React Testing Library: Use
RTLto test component behavior, not implementation details.
❌ BAD: Non-semantic HTML / Untestable implementation
// ❌ Div acting as a button, missing keyboard interaction
function MyButton() {
return <div => alert('Clicked!')}>Click Me</div>;
}
// ❌ Testing internal state or component instance (implementation detail)
test('MyComponent sets count to 1', () => {
const { instance } = render(<MyComponent />);
expect(instance.state.count).toBe(1); // ❌ Avoid
});
✅ GOOD: Semantic HTML / User-centric testing
import { render, screen, fireEvent } from '@testing-library/react';
// ✅ Proper button element with click handler
function MyButton() {
return <button type="button" => alert('Clicked!')}>Click Me</button>;
}
// ✅ Test user interaction and visible output
test('MyButton alerts on click', () => {
render(<MyButton />);
fireEvent.click(screen.getByRole('button', { name: /click me/i }));
expect(window.alert).toHaveBeenCalledWith('Clicked!'); // Assuming alert is mocked
});