Table of Contents
- Naming Conventions
- Component Structure
- State Management
- Props Handling
- Hooks Best Practices
- Performance Optimization
- Error Handling
- Testing
- Code Organization
- Tooling for Enforcement
- Conclusion
- References
1. Naming Conventions
Consistent naming makes code self-documenting and easier to navigate. Follow these rules:
Components & Files
- Use PascalCase for component names (e.g.,
UserProfile,OrderSummary). - Match component filenames to the component name (e.g.,
UserProfile.tsxfor a component namedUserProfile). - Avoid generic names like
ComponentorPage—be specific (e.g.,CheckoutPageinstead ofPage).
Example:
// UserProfile.tsx
import React from 'react';
const UserProfile: React.FC = () => {
return <div>User Profile</div>;
};
export default UserProfile;
Functions & Variables
- Use camelCase for functions, variables, and props (e.g.,
handleSubmit,userData). - Prefix event handlers with
handle(e.g.,handleClick,handleFormSubmit). - Use UPPER_SNAKE_CASE for constants (e.g.,
MAX_ITEMS,API_BASE_URL).
Example:
// Constants
const MAX_ITEMS = 10;
// Event handler
const handleDelete = (id: string) => {
// logic to delete item
};
// Variable
const userSettings = { theme: 'dark' };
Booleans & Enums
- Prefix booleans with
is,has,should, orcan(e.g.,isActive,hasPermission). - Use enums for fixed sets of values (e.g.,
StatuswithPending,Approved).
Example:
// Boolean
const isLoggedIn = true;
const hasAccess = false;
// Enum
enum Status {
Pending = 'pending',
Approved = 'approved',
Rejected = 'rejected',
}
const orderStatus: Status = Status.Pending;
2. Component Structure
A well-structured component is easy to read and modify. Follow this order for clarity:
Typical Component Flow
- Imports: Start with external imports (React, libraries), then internal imports (components, utilities).
- Type Definitions: Interfaces/Types (for TypeScript) or PropTypes (for JavaScript).
- Constants: Static values used in the component.
- Helper Functions: Small, reusable logic (e.g., formatting dates, validating inputs).
- Component Function: The main component logic.
- Exports: Default or named exports.
Example (TypeScript):
// UserProfile.tsx
import React from 'react';
import { Avatar } from '../components/Avatar'; // Internal import
import { formatDate } from '../utils/date-utils'; // Helper utility
// Type definition
interface UserProfileProps {
user: {
id: string;
name: string;
joinDate: string;
};
}
// Constants
const PROFILE_AVATAR_SIZE = 64;
// Helper function
const getMemberSince = (date: string) => `Member since ${formatDate(date)}`;
// Component
const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return (
<div className="profile">
<Avatar size={PROFILE_AVATAR_SIZE} name={user.name} />
<h2>{user.name}</h2>
<p>{getMemberSince(user.joinDate)}</p>
</div>
);
};
export default UserProfile;
Single Responsibility Principle
Each component should do one thing. If a component grows too large (e.g., >200 lines), split it into smaller, reusable components.
Bad:
// Too many responsibilities: rendering list + filtering + pagination
const UserList = () => {
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(1);
// Fetch, filter, and paginate logic...
return (
<div>
<input onChange={(e) => setSearchTerm(e.target.value)} />
<ul>{filteredUsers.map(user => <li>{user.name}</li>)}</ul>
<button onClick={() => setPage(page - 1)}>Prev</button>
<button onClick={() => setPage(page + 1)}>Next</button>
</div>
);
};
Good:
Split into UserSearchInput, UserList, and Pagination components.
3. State Management
Effective state management prevents bugs and keeps logic predictable.
Local vs. Global State
- Local State: Use
useStatefor state specific to a component (e.g., form inputs, UI toggles). - Global State: Use Context API, Redux, or Zustand for state shared across components (e.g., user auth, theme settings).
Example: Local State
const LoginForm = () => {
const [email, setEmail] = useState(''); // Local to LoginForm
const [password, setPassword] = useState(''); // Local to LoginForm
return (
<form>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{/* ... */}
</form>
);
};
Avoid Redundant State
Derive state from existing state/props instead of duplicating it.
Bad:
const UserProfile = ({ user }) => {
const [fullName, setFullName] = useState('');
// Redundant: fullName can be derived from user.firstName + user.lastName
useEffect(() => {
setFullName(`${user.firstName} ${user.lastName}`);
}, [user]);
return <div>{fullName}</div>;
};
Good:
const UserProfile = ({ user }) => {
const fullName = `${user.firstName} ${user.lastName}`; // Derived
return <div>{fullName}</div>;
};
useReducer for Complex State
Use useReducer when state logic involves multiple sub-values or complex transitions (e.g., form validation with multiple steps).
Example:
type State = {
step: number;
formData: { name: string; email: string };
};
type Action =
| { type: 'NEXT_STEP' }
| { type: 'PREV_STEP' }
| { type: 'UPDATE_FIELD'; payload: { name: string; value: string } };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'NEXT_STEP':
return { ...state, step: state.step + 1 };
case 'PREV_STEP':
return { ...state, step: state.step - 1 };
case 'UPDATE_FIELD':
return {
...state,
formData: { ...state.formData, [action.payload.name]: action.payload.value },
};
default:
return state;
}
};
const MultiStepForm = () => {
const [state, dispatch] = useReducer(reducer, {
step: 1,
formData: { name: '', email: '' },
});
return (
<div>
<input
name="name"
value={state.formData.name}
onChange={(e) => dispatch({ type: 'UPDATE_FIELD', payload: { name: e.target.name, value: e.target.value } })}
/>
<button onClick={() => dispatch({ type: 'NEXT_STEP' })}>Next</button>
</div>
);
};
4. Props Handling
Props are the primary way components communicate. Use these practices to keep props manageable.
Validate Props
- TypeScript: Use interfaces/types to enforce prop shapes.
- JavaScript: Use
PropTypesfor runtime validation.
Example (TypeScript):
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary'; // Optional
onClick: () => void; // Required function
}
const Button: React.FC<ButtonProps> = ({ label, variant = 'primary', onClick }) => {
return <button className={`btn-${variant}`} onClick={onClick}>{label}</button>;
};
Example (JavaScript with PropTypes):
import PropTypes from 'prop-types';
const Button = ({ label, variant = 'primary', onClick }) => {
return <button className={`btn-${variant}`} onClick={onClick}>{label}</button>;
};
Button.propTypes = {
label: PropTypes.string.isRequired,
variant: PropTypes.oneOf(['primary', 'secondary']),
onClick: PropTypes.func.isRequired,
};
Destructure Props
Destructure props for readability, especially when passing many props.
Bad:
const UserCard = (props) => {
return (
<div>
<h3>{props.user.name}</h3>
<p>{props.user.email}</p>
<button onClick={props.onEdit}>Edit</button>
</div>
);
};
Good:
const UserCard = ({ user, onEdit }) => {
const { name, email } = user; // Nested destructuring
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
};
Avoid Prop Drilling
Passing props through multiple levels (“prop drilling”) leads to fragile code. Use Context API or composition instead.
Bad (Prop Drilling):
// App → Parent → Child → Grandchild (passing "theme" all the way)
const App = () => <Parent theme="dark" />;
const Parent = ({ theme }) => <Child theme={theme} />;
const Child = ({ theme }) => <Grandchild theme={theme} />;
Good (Context):
// ThemeContext.tsx
const ThemeContext = React.createContext('light');
const App = () => (
<ThemeContext.Provider value="dark">
<Parent />
</ThemeContext.Provider>
);
const Grandchild = () => {
const theme = useContext(ThemeContext); // Direct access
return <div style={{ background: theme }} />;
};
5. Hooks Best Practices
Hooks are powerful, but misusing them causes bugs. Follow these rules:
Rules of Hooks
- Only call hooks at the top level of components/custom hooks (not inside loops, conditionals, or nested functions).
- Only call hooks from React components or custom hooks (not regular functions).
Bad:
const UserList = ({ users }) => {
if (users.length === 0) {
useState('No users'); // ❌ Inside conditional
}
// ...
};
Good:
const UserList = ({ users }) => {
const [message, setMessage] = useState('');
useEffect(() => {
if (users.length === 0) {
setMessage('No users'); // ✅ Inside effect (top-level)
}
}, [users]);
// ...
};
Custom Hooks
Extract reusable logic into custom hooks (prefix with use).
Example: useLocalStorage
// useLocalStorage.ts
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage in component
const [theme, setTheme] = useLocalStorage('theme', 'light');
Avoid Overusing Hooks
Too many hooks in one component make it hard to follow. Split the component or extract logic into a custom hook.
Bad:
const Dashboard = () => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
// 4 useEffect hooks for data fetching...
useEffect(() => { /* fetch user */ }, []);
useEffect(() => { /* fetch posts */ }, [user?.id]);
useEffect(() => { /* fetch comments */ }, [posts]);
useEffect(() => { /* fetch notifications */ }, [user?.id]);
// ...
};
Good:
Extract data fetching into useUserData, usePostsData, etc., or split into UserDashboard, PostsDashboard components.
6. Performance Optimization
Optimize only when necessary, but know these tools:
Memoization
React.memo: Memoize components to prevent re-renders if props haven’t changed.useMemo: Memoize expensive calculations (e.g., sorting large lists).useCallback: Memoize functions passed as props to prevent unnecessary re-renders of child components.
Example: useCallback with React.memo
const Parent = () => {
const [count, setCount] = useState(0);
// Memoize handleClick to avoid recreating it on every render
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty deps: stable reference
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} /> {/* Child won't re-render */}
</div>
);
};
// Memoize Child to skip re-renders if props are the same
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
Lists: Keys and Virtualization
- Always use stable, unique keys in lists (avoid indexes unless the list is static).
- For long lists (>100 items), use virtualization (e.g.,
react-windoworreact-virtualized) to render only visible items.
Example: Key in Lists
// Bad: Index as key (risky if list is reordered/filtered)
{users.map((user, index) => (
<UserItem key={index} user={user} />
))}
// Good: Unique ID as key
{users.map((user) => (
<UserItem key={user.id} user={user} />
))}
7. Error Handling
Prevent crashes and improve UX with robust error handling.
Async Operations
Use try/catch with async logic (e.g., API calls in useEffect).
Example:
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [error, setError] = useState('');
useEffect(() => {
const fetchUser = async () => {
try {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
const data = await res.json();
setUser(data);
} catch (err) {
setError(err.message); // Handle error
}
};
fetchUser();
}, [userId]);
if (error) return <div>Error: {error}</div>; // Show error to user
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
};
Error Boundaries
Wrap components with error boundaries to catch errors and prevent app crashes.
Example: ErrorBoundary Component
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true }; // Update state to show fallback UI
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Error:', error, info); // Log error
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please try again.</div>; // Fallback UI
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<UserProfile userId="123" />
</ErrorBoundary>
8. Testing
Tests ensure code works as expected and prevent regressions.
Unit Tests
Test individual components, hooks, and utilities with Jest and React Testing Library.
Example: Testing a Button Component
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
test('renders label and triggers onClick', () => {
const handleClick = jest.fn();
render(<Button label="Click Me" onClick={handleClick} />);
const button = screen.getByText('Click Me');
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
Integration Tests
Test component interactions (e.g., form submission, data fetching).
Example: Testing a Login Form
// LoginForm.test.tsx
test('submits form with email and password', async () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
// Fill inputs
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: '[email protected]' } });
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'password123' } });
// Submit form
fireEvent.click(screen.getByRole('button', { name: /login/i }));
// Assert submit was called with correct data
expect(mockSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
9. Code Organization
A logical folder structure keeps code scalable and easy to navigate.
Feature-Based Structure
Group files by feature (e.g., auth, dashboard) instead of type (e.g., components, hooks).
Example:
src/
├── features/
│ ├── auth/
│ │ ├── components/ (LoginForm, RegisterForm)
│ │ ├── hooks/ (useLogin, useRegister)
│ │ ├── api.ts (auth API calls)
│ │ ├── types.ts (auth-related types)
│ │ └── AuthContext.tsx
│ ├── dashboard/
│ │ ├── components/ (StatsCard, RecentActivity)
│ │ └── DashboardPage.tsx
├── shared/ (reusable components/hooks used across features)
│ ├── components/ (Button, Input, Modal)
│ └── hooks/ (useLocalStorage, useApi)
└── App.tsx
Avoid Deep Nesting
Limit folder depth to 3–4 levels to avoid long import paths.
Bad:
src/components/user/profile/details/AddressForm.tsx
Good:
src/features/user/components/AddressForm.tsx
Index Files for Exports
Use index.ts files to simplify imports.
Example:
// features/auth/components/index.ts
export { default as LoginForm } from './LoginForm';
export { default as RegisterForm } from './RegisterForm';
// Usage in another file
import { LoginForm, RegisterForm } from '../features/auth/components';
10. Tooling for Enforcement
Automate standards with tools to reduce manual effort.
Linting & Formatting
- ESLint: Enforce code quality rules (e.g., no unused variables, hook rules).
- Prettier: Auto-format code for consistency (indentation, line length).
Setup:
- Install dependencies:
npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev - Configure
.eslintrc.js:module.exports = { extends: [ 'react-app', // Base React config 'plugin:react-hooks/recommended', // Hook rules 'prettier', // Disable ESLint formatting rules ], rules: { 'react/prop-types': 'error', // Enforce prop types 'no-console': 'warn', // Warn on console logs }, }; - Add scripts to
package.json:"scripts": { "lint": "eslint .", "format": "prettier --write ." }
Pre-Commit Hooks
Use Husky and lint-staged to run linting/formatting before commits.
Setup:
npm install husky lint-staged --save-dev
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
Add to package.json:
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
TypeScript
Use TypeScript for type safety, catching errors at compile time.
Example: Type Safety
// Type error caught at compile time
interface User {
id: string;
name: string;
}
const user: User = { id: 123 }; // ❌ Type 'number' is not assignable to type 'string' (id)
Conclusion
Writing clean, maintainable React code isn’t about perfection—it’s about consistency and empathy for your team (and future self). By following these standards—consistent naming, structured components, smart state management, and leveraging tools—you’ll build applications that are easy to debug, scale, and collaborate on.
Remember: standards evolve. Regularly review and update your practices as your team and project grow.