- Introduction
- Project Structure
- Component Architecture
- State Management
- Styling
- Performance Optimization
- Testing
- TypeScript Integration
- Hooks Usage
- Error Handling
- Accessibility
- Security Best Practices
- Code Style
- Documentation
This document outlines the standard conventions and best practices for React application development at Bayat. These guidelines aim to ensure consistency, maintainability, and scalability across all React projects.
src/
├── assets/ # Static files like images, fonts, etc.
├── components/ # Reusable UI components
│ ├── common/ # Shared components across features
│ └── features/ # Feature-specific components
├── config/ # Configuration files
├── contexts/ # React Context definitions
├── hooks/ # Custom React hooks
├── layouts/ # Layout components
├── pages/ # Page components
├── services/ # API services and data fetching
├── store/ # State management (Redux, Zustand, etc.)
├── styles/ # Global styles, themes, variables
├── types/ # TypeScript type definitions
└── utils/ # Utility functions
- Components: Use PascalCase for component files and directories
- Example:
Button.tsx
,UserProfile.tsx
- Example:
- Non-component files: Use camelCase
- Example:
useAuth.ts
,apiService.ts
- Example:
- Test files: Use the same name as the file they test with
.test
or.spec
suffix- Example:
Button.test.tsx
,useAuth.spec.ts
- Example:
- Style files: Use the same name as the component they style
- Example:
Button.module.css
,UserProfile.styles.ts
- Example:
- Follow the single responsibility principle
- Prefer functional components with hooks over class components
- Break down complex components into smaller, reusable pieces
- Keep components under 250 lines of code when possible
- Presentational Components: Focus on UI and receive data via props
- Container Components: Manage state and data fetching, passing data to presentational components
- Layout Components: Handle structural layout and positioning
- Page Components: Represent entire pages and compose other components
- HOCs/Render Props: For cross-cutting concerns when hooks aren't sufficient
- Use destructuring for props
- Define prop types with TypeScript interfaces or PropTypes
- Provide default props when applicable
- Keep required props to a minimum
- Use prop spreading sparingly and only when necessary
// Good
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
label: string;
onClick: () => void;
disabled?: boolean;
}
const Button = ({
variant = 'primary',
size = 'medium',
label,
onClick,
disabled = false
}: ButtonProps) => {
// Component implementation
};
- Use
useState
for simple component-level state - Use
useReducer
for complex state logic within a component
- For small to medium apps, use React Context with hooks
- For larger applications, consider:
- Redux (with Redux Toolkit)
- Zustand
- Jotai
- Recoil
- Keep state as local as possible
- Lift state up only when necessary
- Use memoization to prevent unnecessary re-renders
- Avoid prop drilling by using context or state management libraries
- Structure state to minimize deep nesting
- CSS Modules: Preferred for component-specific styling
- Styled Components/Emotion: For dynamic styling based on props
- Tailwind CSS: Acceptable for rapid prototyping and consistent design systems
- Global CSS: Only for app-wide styles (reset, variables, typography)
- Follow responsive design principles
- Use design tokens for colors, spacing, typography
- Create theme variables for light/dark mode
- Implement mobile-first approach
- Avoid inline styles
- Use relative units (rem, em) over pixels when possible
- Implement code splitting with dynamic imports
- Use React.memo for expensive pure components
- Apply useMemo and useCallback for referential equality
- Employ virtualization for long lists (react-window, react-virtualized)
- Use image optimization techniques
- Implement lazy loading for components and assets
- Avoid unnecessary re-renders with proper key usage
// Good example of React.memo with useCallback
const MemoizedComponent = React.memo(({ onItemClick, items }) => {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// Parent component
const Parent = () => {
const [items, setItems] = useState([]);
const handleItemClick = useCallback((id) => {
// Handle click logic
}, [/* dependencies */]);
return <MemoizedComponent onItemClick={handleItemClick} items={items} />;
};
- Unit Tests: For individual components and utilities
- Integration Tests: For component interactions
- E2E Tests: For critical user flows
- Jest as the test runner
- React Testing Library for component testing
- Cypress or Playwright for E2E testing
- MSW for API mocking
- Write tests that mirror user behavior
- Test component behavior, not implementation details
- Aim for meaningful test coverage, not just metrics
- Mock external dependencies
- Use test-driven development when appropriate
// Good example of a component test
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button', () => {
it('renders with the correct label', () => {
render(<Button label="Click me" onClick={jest.fn()} />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click me" onClick={handleClick} />);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
- Use TypeScript for all new React projects
- Define interfaces for component props, state, and context
- Use discriminated unions for complex state
- Leverage utility types (Partial, Omit, Pick, etc.)
- Create reusable type definitions
- Avoid using
any
andas
type assertions
// Good example of TypeScript usage
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface UserCardProps {
user: User;
onEdit?: (id: string) => void;
editable?: boolean;
}
const UserCard = ({ user, onEdit, editable = false }: UserCardProps) => {
// Component implementation
};
- Extract reusable logic into custom hooks
- Name hooks with 'use' prefix
- Keep hooks focused on a single concern
- Document hook parameters and return values
- Handle errors gracefully within hooks
// Good example of a custom hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
- useState: Split complex state into multiple useState calls
- useEffect: Specify dependencies accurately to prevent infinite loops
- useContext: Create custom hooks to consume context
- useReducer: Use for complex state logic
- useCallback/useMemo: Apply only when performance optimizations are needed
- Implement error boundaries for component-level error catching
- Use try/catch for async operations
- Create standardized error display components
- Log errors to monitoring services
- Provide user-friendly error messages
- Implement recovery mechanisms where possible
// Error boundary example
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log to error monitoring service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorDisplay error={this.state.error} />;
}
return this.props.children;
}
}
- Follow WCAG 2.1 AA standards at minimum
- Use semantic HTML elements
- Implement proper keyboard navigation
- Provide ARIA attributes when necessary
- Ensure sufficient color contrast
- Test with screen readers
- Support reduced motion preferences
// Good accessibility example
const ActionButton = ({ onClick, label, isDisabled = false }) => {
return (
<button
onClick={onClick}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-label={label}
>
{label}
</button>
);
};
- Sanitize user input
- Prevent XSS via proper escaping
- Use React's built-in protections against injection
- Implement proper authentication and authorization
- Protect against CSRF
- Follow least privilege principle for API calls
- Keep dependencies updated for security patches
- Follow ESLint and Prettier configurations
- Use consistent naming conventions
- Apply functional programming principles where appropriate
- Write self-documenting code
- Keep functions pure when possible
- Use destructuring for props and state
- Apply consistent patterns across the codebase
- Document component props with JSDoc comments
- Create Storybook stories for UI components
- Maintain a component library documentation
- Document complex logic and algorithms
- Keep README files updated
- Include setup instructions for new developers
- Document state management approaches
/**
* Button component for common actions
*
* @param {Object} props - Component props
* @param {string} props.label - Button text
* @param {'primary' | 'secondary' | 'tertiary'} [props.variant='primary'] - Visual style variant
* @param {'small' | 'medium' | 'large'} [props.size='medium'] - Button size
* @param {() => void} props.onClick - Click handler function
* @param {boolean} [props.disabled=false] - Whether the button is disabled
* @param {React.ReactNode} [props.icon] - Optional icon to display
*/
const Button = ({
label,
variant = 'primary',
size = 'medium',
onClick,
disabled = false,
icon
}) => {
// Implementation
};
Version | Date | Description |
---|---|---|
1.0 | 2025-03-20 | Initial version |