What are your preferred patterns for handling side effects in React?
Answer:
1. useEffect Hook Patterns:
// Basic data fetching pattern
const useUserData = (userId) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchUser();
return () => {
cancelled = true; // Cleanup to prevent state updates on unmounted component
};
}, [userId]);
return { user, loading, error };
};
2. Custom Hooks for Reusable Side Effects:
// Debounced search hook
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
// Usage
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { results, loading } = useSearchResults(debouncedSearchTerm);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
{results.map(result => <div key={result.id}>{result.title}</div>)}
</div>
);
};
3. Event Listeners and Cleanup:
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
};
4. Subscription Management:
const useWebSocket = (url) => {
const [socket, setSocket] = useState(null);
const [lastMessage, setLastMessage] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
setConnectionStatus('connected');
setSocket(ws);
};
ws.onmessage = (event) => {
setLastMessage(JSON.parse(event.data));
};
ws.onclose = () => {
setConnectionStatus('disconnected');
setSocket(null);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('error');
};
return () => {
ws.close();
};
}, [url]);
const sendMessage = useCallback((message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}, [socket]);
return { lastMessage, connectionStatus, sendMessage };
};
5. Advanced Patterns with useReducer:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
loading: true,
error: null,
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
data: action.payload,
error: null,
};
case 'FETCH_FAILURE':
return {
...state,
loading: false,
error: action.payload,
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
const useDataFetcher = (initialUrl) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
data: null,
loading: false,
error: null,
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await fetch(url);
const data = await result.json();
if (!cancelled) {
dispatch({ type: 'FETCH_SUCCESS', payload: data });
}
} catch (error) {
if (!cancelled) {
dispatch({ type: 'FETCH_FAILURE', payload: error.message });
}
}
};
if (url) {
fetchData();
}
return () => {
cancelled = true;
};
}, [url]);
return { ...state, setUrl };
};
6. Best Practices:
- Always provide cleanup functions to prevent memory leaks
- Use dependency arrays correctly to avoid infinite loops
- Extract complex side effects into custom hooks for reusability
- Handle loading and error states consistently
- Use useCallback and useMemo to prevent unnecessary re-renders
- Consider using libraries like React Query for complex data fetching scenarios
How do you ensure type safety when working with external APIs?
Answer:
1. API Response Type Definitions:
// Define API response types
interface User {
id: number;
name: string;
email: string;
avatar?: string;
createdAt: string;
updatedAt: string;
}
interface ApiResponse<T> {
data: T;
message: string;
status: 'success' | 'error';
timestamp: string;
}
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
message: string;
status: 'success' | 'error';
}
// Specific API response types
type UserResponse = ApiResponse<User>;
type UsersListResponse = PaginatedResponse<User>;
2. API Client with Type Safety:
class ApiClient {
private baseURL: string;
private defaultHeaders: Record<string, string>;
constructor(baseURL: string) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
...options,
headers: {
...this.defaultHeaders,
...options.headers,
},
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data as T;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Typed API methods
async getUsers(page: number = 1, limit: number = 10): Promise<UsersListResponse> {
return this.request<UsersListResponse>(`/users?page=${page}&limit=${limit}`);
}
async getUser(id: number): Promise<UserResponse> {
return this.request<UserResponse>(`/users/${id}`);
}
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<UserResponse> {
return this.request<UserResponse>('/users', {
method: 'POST',
body: JSON.stringify(userData),
});
}
async updateUser(id: number, userData: Partial<User>): Promise<UserResponse> {
return this.request<UserResponse>(`/users/${id}`, {
method: 'PUT',
body: JSON.stringify(userData),
});
}
}
3. Runtime Validation with Zod:
import { z } from 'zod';
// Define schemas for runtime validation
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
avatar: z.string().url().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
const ApiResponseSchema = z.object({
data: z.any(),
message: z.string(),
status: z.enum(['success', 'error']),
timestamp: z.string(),
});
// Type-safe API client with validation
class ValidatedApiClient extends ApiClient {
async getUsers(page: number = 1, limit: number = 10): Promise<UsersListResponse> {
const response = await this.request(`/users?page=${page}&limit=${limit}`);
// Runtime validation
const validatedResponse = ApiResponseSchema.parse(response);
// Validate the data array
if (Array.isArray(validatedResponse.data)) {
validatedResponse.data.forEach(user => UserSchema.parse(user));
}
return validatedResponse as UsersListResponse;
}
async getUser(id: number): Promise<UserResponse> {
const response = await this.request(`/users/${id}`);
// Validate the response structure
const validatedResponse = ApiResponseSchema.parse(response);
// Validate the user data
const validatedUser = UserSchema.parse(validatedResponse.data);
return {
...validatedResponse,
data: validatedUser,
} as UserResponse;
}
}
4. React Hooks with Type Safety:
// Custom hook for API calls
const useApi = <T>(apiCall: () => Promise<T>) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const result = await apiCall();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'An error occurred');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [apiCall]);
return { data, loading, error };
};
// Usage with type safety
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const { data: userResponse, loading, error } = useApi<UserResponse>(
() => apiClient.getUser(userId)
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!userResponse?.data) return <div>User not found</div>;
const user = userResponse.data; // TypeScript knows this is a User
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
{user.avatar && <img src={user.avatar} alt={user.name} />}
</div>
);
};
5. Error Handling with Discriminated Unions:
// Define error types
interface ApiError {
type: 'API_ERROR';
message: string;
statusCode: number;
details?: Record<string, any>;
}
interface ValidationError {
type: 'VALIDATION_ERROR';
message: string;
field: string;
value: any;
}
interface NetworkError {
type: 'NETWORK_ERROR';
message: string;
originalError: Error;
}
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: ApiError | ValidationError | NetworkError };
// Type-safe error handling
const safeApiCall = async <T>(
apiCall: () => Promise<T>
): Promise<ApiResult<T>> => {
try {
const data = await apiCall();
return { success: true, data };
} catch (error) {
if (error instanceof TypeError && error.message.includes('fetch')) {
return {
success: false,
error: {
type: 'NETWORK_ERROR',
message: 'Network request failed',
originalError: error,
},
};
}
if (error instanceof z.ZodError) {
return {
success: false,
error: {
type: 'VALIDATION_ERROR',
message: 'Data validation failed',
field: error.errors[0]?.path.join('.') || 'unknown',
value: error.errors[0]?.received,
},
};
}
return {
success: false,
error: {
type: 'API_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
statusCode: 500,
},
};
}
};
6. Best Practices:
- Always define interfaces for API responses
- Use runtime validation with libraries like Zod or Yup
- Implement proper error handling with discriminated unions
- Create type-safe API clients with generic methods
- Use custom hooks to encapsulate API logic
- Validate data at runtime to catch API changes
- Use TypeScript strict mode for better type checking
- Document API contracts and keep types in sync
What's your approach to styling in React applications? (CSS Modules, Styled Components, Tailwind, etc.)
Answer:
1. Tailwind CSS (Preferred Approach):
// Component with Tailwind classes
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
children,
...props
}) => {
const baseClasses = 'font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
{...props}
>
{children}
</button>
);
};
// Responsive design with Tailwind
const Dashboard: React.FC = () => {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<h1 className="text-xl font-semibold text-gray-900">Dashboard</h1>
<nav className="hidden md:flex space-x-8">
<a href="#" className="text-gray-500 hover:text-gray-900">Home</a>
<a href="#" className="text-gray-500 hover:text-gray-900">Analytics</a>
</nav>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
<span className="text-white text-sm font-medium">📊</span>
</div>
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate">
Total Revenue
</dt>
<dd className="text-lg font-medium text-gray-900">
$45,231.89
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
};
2. CSS Modules:
// Button.module.css
.button {
@apply font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
}
.primary {
@apply bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
}
.secondary {
@apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500;
}
.small {
@apply px-3 py-1.5 text-sm;
}
.medium {
@apply px-4 py-2 text-base;
}
.large {
@apply px-6 py-3 text-lg;
}
// Button.tsx
import styles from './Button.module.css';
import { clsx } from 'clsx';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
className,
children,
...props
}) => {
return (
<button
className={clsx(
styles.button,
styles[variant],
styles[size],
className
)}
{...props}
>
{children}
</button>
);
};
3. Styled Components:
import styled, { css } from 'styled-components';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
}
const StyledButton = styled.button<ButtonProps>`
font-weight: 500;
border-radius: 0.5rem;
transition: all 0.2s ease-in-out;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
${({ variant = 'primary' }) => {
switch (variant) {
case 'primary':
return css`
background-color: #2563eb;
color: white;
&:hover {
background-color: #1d4ed8;
}
`;
case 'secondary':
return css`
background-color: #e5e7eb;
color: #111827;
&:hover {
background-color: #d1d5db;
}
`;
case 'danger':
return css`
background-color: #dc2626;
color: white;
&:hover {
background-color: #b91c1c;
}
`;
}
}}
${({ size = 'md' }) => {
switch (size) {
case 'sm':
return css`
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
`;
case 'md':
return css`
padding: 0.5rem 1rem;
font-size: 1rem;
`;
case 'lg':
return css`
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
`;
}
}}
${({ fullWidth }) =>
fullWidth &&
css`
width: 100%;
`}
`;
// Theme provider setup
const theme = {
colors: {
primary: '#2563eb',
secondary: '#6b7280',
success: '#10b981',
danger: '#dc2626',
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
breakpoints: {
mobile: '768px',
tablet: '1024px',
desktop: '1280px',
},
};
const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <StyledThemeProvider theme={theme}>{children}</StyledThemeProvider>;
};
4. Emotion (CSS-in-JS Alternative):
import { css } from '@emotion/react';
import styled from '@emotion/styled';
// Using css prop
const Card: React.FC = () => {
return (
<div
css={css`
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1rem;
&:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
transition: all 0.2s ease-in-out;
}
`}
>
<h3
css={css`
margin: 0 0 1rem 0;
color: #1f2937;
font-size: 1.25rem;
font-weight: 600;
`}
>
Card Title
</h3>
<p
css={css`
margin: 0;
color: #6b7280;
line-height: 1.5;
`}
>
Card content goes here...
</p>
</div>
);
};
// Using styled components with Emotion
const StyledInput = styled.input`
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 1rem;
&:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
&::placeholder {
color: #9ca3af;
}
`;
5. Design System Approach:
// Design tokens
export const tokens = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
gray: {
50: '#f9fafb',
500: '#6b7280',
900: '#111827',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
typography: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
fontSize: {
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
},
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
};
// Utility functions
export const createResponsiveValue = <T>(values: Partial<Record<keyof typeof tokens.breakpoints, T>>) => {
return Object.entries(values)
.map(([breakpoint, value]) => {
const minWidth = tokens.breakpoints[breakpoint as keyof typeof tokens.breakpoints];
return `@media (min-width: ${minWidth}) { ${value} }`;
})
.join(' ');
};
6. Best Practices & Recommendations:
When to use each approach:
- Tailwind CSS: Best for rapid prototyping, consistent design systems, and utility-first development
- CSS Modules: Good for component-scoped styles with traditional CSS syntax
- Styled Components: Ideal for dynamic styling based on props and complex component logic
- Emotion: Great alternative to Styled Components with better performance
- CSS-in-JS: Best for applications requiring runtime theming and dynamic styles
General Guidelines:
- Consistency: Choose one primary approach and stick to it
- Performance: Consider bundle size and runtime performance
- Developer Experience: Ensure good TypeScript support and tooling
- Maintainability: Use design tokens and consistent naming conventions
- Accessibility: Ensure styles support keyboard navigation and screen readers
- Responsive Design: Always consider mobile-first approach
- Theme Support: Plan for dark mode and theme switching if needed
How do you handle internationalization (i18n) and accessibility (a11y) in React apps?
Answer:
1. Internationalization (i18n) with react-i18next:
// i18n configuration
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// Translation resources
const resources = {
en: {
translation: {
welcome: 'Welcome',
hello: 'Hello {{name}}!',
buttons: {
save: 'Save',
cancel: 'Cancel',
delete: 'Delete',
},
messages: {
success: 'Operation completed successfully',
error: 'An error occurred',
confirm: 'Are you sure you want to delete this item?',
},
navigation: {
home: 'Home',
about: 'About',
contact: 'Contact',
},
},
},
es: {
translation: {
welcome: 'Bienvenido',
hello: '¡Hola {{name}}!',
buttons: {
save: 'Guardar',
cancel: 'Cancelar',
delete: 'Eliminar',
},
messages: {
success: 'Operación completada exitosamente',
error: 'Ocurrió un error',
confirm: '¿Estás seguro de que quieres eliminar este elemento?',
},
navigation: {
home: 'Inicio',
about: 'Acerca de',
contact: 'Contacto',
},
},
},
};
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
debug: process.env.NODE_ENV === 'development',
interpolation: {
escapeValue: false, // React already escapes values
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
},
});
export default i18n;
2. Using i18n in Components:
import { useTranslation } from 'react-i18next';
// Basic usage
const WelcomePage: React.FC = () => {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('hello', { name: 'John' })}</p>
<div>
<button onClick={() => changeLanguage('en')}>English</button>
<button onClick={() => changeLanguage('es')}>Español</button>
</div>
</div>
);
};
// Advanced usage with namespaces
const UserProfile: React.FC = () => {
const { t } = useTranslation(['user', 'common']);
return (
<div>
<h2>{t('user:profile.title')}</h2>
<p>{t('user:profile.description')}</p>
<button>{t('common:buttons.save')}</button>
</div>
);
};
// Custom hook for complex translations
const useLocalizedDate = (date: Date) => {
const { i18n } = useTranslation();
return useMemo(() => {
return new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}, [date, i18n.language]);
};
3. Accessibility (a11y) Implementation:
// Accessible form component
const AccessibleForm: React.FC = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Form validation logic
};
return (
<form onSubmit={handleSubmit} noValidate>
<fieldset>
<legend>Contact Information</legend>
<div className="form-group">
<label htmlFor="name" className="required">
Full Name
<span className="sr-only"> (required)</span>
</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
aria-describedby={errors.name ? 'name-error' : undefined}
aria-invalid={!!errors.name}
required
/>
{errors.name && (
<div id="name-error" role="alert" className="error-message">
{errors.name}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="email" className="required">
Email Address
<span className="sr-only"> (required)</span>
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={!!errors.email}
required
/>
{errors.email && (
<div id="email-error" role="alert" className="error-message">
{errors.email}
</div>
)}
</div>
<div className="form-group">
<label htmlFor="message">Message</label>
<textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
rows={4}
aria-describedby="message-help"
/>
<div id="message-help" className="help-text">
Please provide details about your inquiry.
</div>
</div>
<button type="submit" className="primary-button">
Send Message
</button>
</fieldset>
</form>
);
};
4. Accessible Navigation:
const AccessibleNavigation: React.FC = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuRef = useRef<HTMLUListElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
const closeMenu = () => {
setIsMenuOpen(false);
buttonRef.current?.focus();
};
// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
closeMenu();
}
};
// Handle focus management
useEffect(() => {
if (isMenuOpen && menuRef.current) {
const firstMenuItem = menuRef.current.querySelector('a');
firstMenuItem?.focus();
}
}, [isMenuOpen]);
return (
<nav role="navigation" aria-label="Main navigation">
<button
ref={buttonRef}
onClick={toggleMenu}
aria-expanded={isMenuOpen}
aria-controls="main-menu"
aria-haspopup="true"
className="menu-toggle"
>
<span className="sr-only">
{isMenuOpen ? 'Close' : 'Open'} main menu
</span>
<span aria-hidden="true">☰</span>
</button>
<ul
ref={menuRef}
id="main-menu"
className={`main-menu ${isMenuOpen ? 'open' : ''}`}
onKeyDown={handleKeyDown}
role="menubar"
>
<li role="none">
<a href="/" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
Home
</a>
</li>
<li role="none">
<a href="/about" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
About
</a>
</li>
<li role="none">
<a href="/contact" role="menuitem" tabIndex={isMenuOpen ? 0 : -1}>
Contact
</a>
</li>
</ul>
</nav>
);
};
5. Accessible Data Tables:
interface TableData {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
}
const AccessibleTable: React.FC<{ data: TableData[] }> = ({ data }) => {
const [sortConfig, setSortConfig] = useState<{
key: keyof TableData;
direction: 'asc' | 'desc';
} | null>(null);
const handleSort = (key: keyof TableData) => {
setSortConfig(prev => ({
key,
direction: prev?.key === key && prev.direction === 'asc' ? 'desc' : 'asc',
}));
};
const sortedData = useMemo(() => {
if (!sortConfig) return data;
return [...data].sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}, [data, sortConfig]);
return (
<div className="table-container">
<table role="table" aria-label="User data table">
<caption className="sr-only">
User data table with sortable columns
</caption>
<thead>
<tr role="row">
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
aria-sort={
sortConfig?.key === 'name'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Name
<span className="sr-only">
{sortConfig?.key === 'name'
? `Sorted ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}`
: 'Click to sort'}
</span>
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('email')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('email')}
aria-sort={
sortConfig?.key === 'email'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Email
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('role')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('role')}
aria-sort={
sortConfig?.key === 'role'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Role
</th>
<th
role="columnheader"
tabIndex={0}
onClick={() => handleSort('status')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('status')}
aria-sort={
sortConfig?.key === 'status'
? sortConfig.direction === 'asc' ? 'ascending' : 'descending'
: 'none'
}
>
Status
</th>
</tr>
</thead>
<tbody>
{sortedData.map((row) => (
<tr key={row.id} role="row">
<td role="cell">{row.name}</td>
<td role="cell">{row.email}</td>
<td role="cell">{row.role}</td>
<td role="cell">
<span
className={`status-badge ${row.status}`}
aria-label={`Status: ${row.status}`}
>
{row.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
6. Custom Accessibility Hooks:
// Focus management hook
const useFocusManagement = () => {
const focusableElements = useRef<HTMLElement[]>([]);
const trapFocus = (container: HTMLElement) => {
const focusable = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<HTMLElement>;
focusableElements.current = Array.from(focusable);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const firstElement = focusableElements.current[0];
const lastElement = focusableElements.current[focusableElements.current.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
};
container.addEventListener('keydown', handleKeyDown);
return () => {
container.removeEventListener('keydown', handleKeyDown);
};
};
return { trapFocus };
};
// Screen reader announcements
const useScreenReaderAnnouncement = () => {
const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}, []);
return { announce };
};
7. Best Practices:
Internationalization:
- Use semantic keys for translations
- Support pluralization and interpolation
- Handle RTL languages properly
- Test with different text lengths
- Use proper date/number formatting
- Consider cultural differences in UI patterns
Accessibility:
- Use semantic HTML elements
- Provide proper ARIA labels and roles
- Ensure keyboard navigation works
- Maintain proper focus management
- Test with screen readers
- Use sufficient color contrast
- Provide alternative text for images
- Make interactive elements large enough
- Use proper heading hierarchy
- Test with real users with disabilities
What tools and processes do you use for maintaining code quality? (ESLint, Prettier, Husky, CI/CD, etc.)
Answer:
1. ESLint Configuration:
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"@typescript-eslint",
"react",
"react-hooks",
"jsx-a11y",
"import"
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"jsx-a11y/anchor-is-valid": "error",
"import/order": [
"error",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index"
],
"newlines-between": "always"
}
]
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
}
}
2. Prettier Configuration:
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"proseWrap": "preserve"
}
// .prettierignore
node_modules/
dist/
build/
.next/
coverage/
*.min.js
*.min.css
package-lock.json
yarn.lock
3. Husky Git Hooks:
// package.json
{
"scripts": {
"lint": "eslint src --ext .ts,.tsx --max-warnings 0",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
"type-check": "tsc --noEmit",
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"build": "tsc && vite build",
"prepare": "husky install"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run type-check && npm run test"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{js,jsx,json,css,md}": [
"prettier --write"
]
}
}
4. TypeScript Configuration:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/hooks/*": ["hooks/*"],
"@/utils/*": ["utils/*"],
"@/types/*": ["types/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"build"
]
}
5. Jest Testing Configuration:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/setupTests.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{ts,tsx}',
'<rootDir>/src/**/*.{test,spec}.{ts,tsx}',
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};
// src/setupTests.ts
import '@testing-library/jest-dom';
import { configure } from '@testing-library/react';
// Configure testing library
configure({ testIdAttribute: 'data-testid' });
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};
6. CI/CD Pipeline (GitHub Actions):
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
quality-checks:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type checking
run: npm run type-check
- name: Linting
run: npm run lint
- name: Format checking
run: npm run format:check
- name: Run tests
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: quality-checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to production
run: |
# Deployment commands here
echo "Deploying to production..."
7. Additional Quality Tools:
// package.json - Additional dev dependencies
{
"devDependencies": {
"@commitlint/cli": "^17.0.0",
"@commitlint/config-conventional": "^17.0.0",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.0",
"npm-run-all": "^4.1.5",
"plop": "^3.0.0",
"size-limit": "^8.0.0",
"@size-limit/preset-small-lib": "^8.0.0"
}
}
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'chore',
'ci',
'build',
'revert'
]
]
}
};
// .size-limit.json
[
{
"path": "dist/index.js",
"limit": "10 KB"
},
{
"path": "dist/index.css",
"limit": "2 KB"
}
]
8. VS Code Configuration:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"emmet.includeLanguages": {
"typescript": "html",
"typescriptreact": "html"
},
"files.associations": {
"*.css": "tailwindcss"
}
}
// .vscode/extensions.json
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense",
"ms-vscode.vscode-json"
]
}
9. Automated Code Generation:
// plopfile.js
module.exports = function (plop) {
plop.setGenerator('component', {
description: 'Create a new React component',
prompts: [
{
type: 'input',
name: 'name',
message: 'Component name:',
},
{
type: 'confirm',
name: 'withProps',
message: 'Include props interface?',
default: true,
},
{
type: 'confirm',
name: 'withTests',
message: 'Include test file?',
default: true,
},
],
actions: [
{
type: 'add',
path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
templateFile: 'templates/component.hbs',
},
{
type: 'add',
path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx',
templateFile: 'templates/component.test.hbs',
skip: (data) => !data.withTests,
},
{
type: 'add',
path: 'src/components/{{pascalCase name}}/index.ts',
templateFile: 'templates/index.hbs',
},
],
});
};
10. Best Practices Summary:
Code Quality Tools:
- ESLint: Catch bugs and enforce coding standards
- Prettier: Consistent code formatting
- TypeScript: Type safety and better developer experience
- Husky: Git hooks for pre-commit checks
- lint-staged: Run linters only on staged files
- Jest: Unit testing with coverage reports
- Commitlint: Enforce conventional commit messages
Processes:
- Pre-commit hooks: Automatic linting and formatting
- CI/CD pipeline: Automated testing and deployment
- Code reviews: Peer review process with quality gates
- Automated testing: Unit, integration, and E2E tests
- Performance monitoring: Bundle size limits and performance budgets
- Documentation: Automated API documentation generation
Quality Metrics:
- Code coverage thresholds (80%+)
- Bundle size limits
- Performance budgets
- Accessibility compliance
- Security vulnerability scanning
- Dependency audit and updates