Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save NiclasOlofsson/ae2d33c04e3320a10966a3230dfd910c to your computer and use it in GitHub Desktop.

Select an option

Save NiclasOlofsson/ae2d33c04e3320a10966a3230dfd910c to your computer and use it in GitHub Desktop.
React Development Guidelines - Modern React development patterns and best practices
description
Modern React development patterns, component design principles, and best practices for building scalable React applications

React Development Guidelines

This comprehensive guide covers modern React development patterns, component design principles, and best practices for building scalable, maintainable React applications using the latest features and ecosystem tools.

Project Structure and Organization

Recommended Folder Structure

src/
├── components/           # Reusable components
│   ├── ui/              # Basic UI components (Button, Input, etc.)
│   ├── layout/          # Layout components (Header, Footer, Sidebar)
│   └── features/        # Feature-specific components
├── hooks/               # Custom hooks
├── contexts/            # React context providers
├── services/            # API calls and external services
├── utils/               # Helper functions and utilities
├── types/               # TypeScript type definitions
├── constants/           # Application constants
├── assets/              # Static assets (images, fonts, etc.)
├── styles/              # Global styles and theme
└── __tests__/           # Test files

Component Organization

// components/features/UserProfile/index.ts
export { UserProfile } from './UserProfile';
export { UserProfileSkeleton } from './UserProfileSkeleton';
export type { UserProfileProps } from './types';

// components/features/UserProfile/UserProfile.tsx
import React from 'react';
import { UserProfileProps } from './types';
import { UserAvatar } from './UserAvatar';
import { UserDetails } from './UserDetails';
import styles from './UserProfile.module.css';

export const UserProfile: React.FC<UserProfileProps> = ({ user, onEdit }) => {
  return (
    <div className={styles.container}>
      <UserAvatar src={user.avatar} alt={user.name} />
      <UserDetails user={user} onEdit={onEdit} />
    </div>
  );
};

Component Design Patterns

Functional Components with TypeScript

import React, { useState, useEffect, useCallback } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
}

interface UserCardProps {
  user: User;
  onUserUpdate?: (user: User) => void;
  onUserDelete?: (userId: string) => void;
  className?: string;
  children?: React.ReactNode;
}

export const UserCard: React.FC<UserCardProps> = ({
  user,
  onUserUpdate,
  onUserDelete,
  className = '',
  children
}) => {
  const [isEditing, setIsEditing] = useState(false);
  const [localUser, setLocalUser] = useState(user);

  // Update local state when user prop changes
  useEffect(() => {
    setLocalUser(user);
  }, [user]);

  const handleSave = useCallback(() => {
    onUserUpdate?.(localUser);
    setIsEditing(false);
  }, [localUser, onUserUpdate]);

  const handleCancel = useCallback(() => {
    setLocalUser(user);
    setIsEditing(false);
  }, [user]);

  return (
    <div className={`user-card ${className}`}>
      {isEditing ? (
        <UserEditForm 
          user={localUser}
          onChange={setLocalUser}
          onSave={handleSave}
          onCancel={handleCancel}
        />
      ) : (
        <UserDisplay 
          user={localUser}
          onEdit={() => setIsEditing(true)}
          onDelete={() => onUserDelete?.(user.id)}
        />
      )}
      {children}
    </div>
  );
};

Compound Components Pattern

import React, { createContext, useContext } from 'react';

interface TabsContextValue {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabsContext = createContext<TabsContextValue | null>(null);

const useTabs = () => {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error('Tab components must be used within Tabs');
  }
  return context;
};

interface TabsProps {
  defaultTab: string;
  children: React.ReactNode;
}

export const Tabs: React.FC<TabsProps> = ({ defaultTab, children }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
};

const TabList: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <div className="tab-list" role="tablist">
    {children}
  </div>
);

interface TabProps {
  value: string;
  children: React.ReactNode;
}

const Tab: React.FC<TabProps> = ({ value, children }) => {
  const { activeTab, setActiveTab } = useTabs();
  
  return (
    <button
      className={`tab ${activeTab === value ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
      role="tab"
      aria-selected={activeTab === value}
    >
      {children}
    </button>
  );
};

const TabPanels: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <div className="tab-panels">{children}</div>
);

const TabPanel: React.FC<TabProps> = ({ value, children }) => {
  const { activeTab } = useTabs();
  
  if (activeTab !== value) return null;
  
  return (
    <div className="tab-panel" role="tabpanel">
      {children}
    </div>
  );
};

// Export compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;

// Usage
<Tabs defaultTab="profile">
  <Tabs.List>
    <Tabs.Tab value="profile">Profile</Tabs.Tab>
    <Tabs.Tab value="settings">Settings</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panels>
    <Tabs.Panel value="profile">Profile content</Tabs.Panel>
    <Tabs.Panel value="settings">Settings content</Tabs.Panel>
  </Tabs.Panels>
</Tabs>

Custom Hooks

Data Fetching Hook

import { useState, useEffect, useCallback, useRef } from 'react';

interface UseAsyncState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
  refetch: () => void;
}

export function useAsync<T>(
  asyncFunction: () => Promise<T>,
  dependencies: React.DependencyList = []
): UseAsyncState<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const cancelRef = useRef<boolean>(false);

  const execute = useCallback(async () => {
    cancelRef.current = false;
    setLoading(true);
    setError(null);

    try {
      const result = await asyncFunction();
      if (!cancelRef.current) {
        setData(result);
      }
    } catch (err) {
      if (!cancelRef.current) {
        setError(err instanceof Error ? err : new Error('Unknown error'));
      }
    } finally {
      if (!cancelRef.current) {
        setLoading(false);
      }
    }
  }, dependencies);

  useEffect(() => {
    execute();
    return () => {
      cancelRef.current = true;
    };
  }, [execute]);

  const refetch = useCallback(() => {
    execute();
  }, [execute]);

  return { data, loading, error, refetch };
}

// Usage
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const { data: user, loading, error, refetch } = useAsync(
    () => fetchUser(userId),
    [userId]
  );

  if (loading) return <UserSkeleton />;
  if (error) return <ErrorMessage error={error} onRetry={refetch} />;
  if (!user) return <UserNotFound />;

  return <UserCard user={user} />;
};

Form Management Hook

import { useState, useCallback } from 'react';

interface ValidationRule<T> {
  validate: (value: T) => boolean;
  message: string;
}

interface FieldConfig<T> {
  initialValue: T;
  validationRules?: ValidationRule<T>[];
}

interface FormConfig {
  [key: string]: FieldConfig<any>;
}

export function useForm<T extends Record<string, any>>(config: FormConfig) {
  const [values, setValues] = useState<T>(() => {
    const initialValues = {} as T;
    Object.keys(config).forEach(key => {
      initialValues[key as keyof T] = config[key].initialValue;
    });
    return initialValues;
  });

  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});

  const setValue = useCallback(<K extends keyof T>(
    field: K,
    value: T[K]
  ) => {
    setValues(prev => ({ ...prev, [field]: value }));
    
    // Clear error when user starts typing
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: undefined }));
    }
  }, [errors]);

  const setFieldTouched = useCallback(<K extends keyof T>(field: K) => {
    setTouched(prev => ({ ...prev, [field]: true }));
  }, []);

  const validateField = useCallback(<K extends keyof T>(field: K): string | null => {
    const fieldConfig = config[field as string];
    if (!fieldConfig?.validationRules) return null;

    const value = values[field];
    for (const rule of fieldConfig.validationRules) {
      if (!rule.validate(value)) {
        return rule.message;
      }
    }
    return null;
  }, [config, values]);

  const validateForm = useCallback(() => {
    const newErrors: Partial<Record<keyof T, string>> = {};
    let isValid = true;

    Object.keys(config).forEach(key => {
      const error = validateField(key as keyof T);
      if (error) {
        newErrors[key as keyof T] = error;
        isValid = false;
      }
    });

    setErrors(newErrors);
    return isValid;
  }, [config, validateField]);

  const handleSubmit = useCallback((onSubmit: (values: T) => void) => {
    return (e: React.FormEvent) => {
      e.preventDefault();
      
      // Mark all fields as touched
      const allTouched = {} as Record<keyof T, boolean>;
      Object.keys(config).forEach(key => {
        allTouched[key as keyof T] = true;
      });
      setTouched(allTouched);

      if (validateForm()) {
        onSubmit(values);
      }
    };
  }, [values, validateForm, config]);

  return {
    values,
    errors,
    touched,
    setValue,
    setFieldTouched,
    validateForm,
    handleSubmit,
    isValid: Object.keys(errors).length === 0
  };
}

// Usage
const LoginForm: React.FC = () => {
  const form = useForm({
    email: {
      initialValue: '',
      validationRules: [
        {
          validate: (value: string) => value.length > 0,
          message: 'Email is required'
        },
        {
          validate: (value: string) => /\S+@\S+\.\S+/.test(value),
          message: 'Invalid email format'
        }
      ]
    },
    password: {
      initialValue: '',
      validationRules: [
        {
          validate: (value: string) => value.length >= 8,
          message: 'Password must be at least 8 characters'
        }
      ]
    }
  });

  const handleLogin = (values: { email: string; password: string }) => {
    console.log('Login with:', values);
  };

  return (
    <form onSubmit={form.handleSubmit(handleLogin)}>
      <input
        type="email"
        value={form.values.email}
        onChange={(e) => form.setValue('email', e.target.value)}
        onBlur={() => form.setFieldTouched('email')}
        placeholder="Email"
      />
      {form.touched.email && form.errors.email && (
        <span className="error">{form.errors.email}</span>
      )}

      <input
        type="password"
        value={form.values.password}
        onChange={(e) => form.setValue('password', e.target.value)}
        onBlur={() => form.setFieldTouched('password')}
        placeholder="Password"
      />
      {form.touched.password && form.errors.password && (
        <span className="error">{form.errors.password}</span>
      )}

      <button type="submit" disabled={!form.isValid}>
        Login
      </button>
    </form>
  );
};

State Management

Context API for Global State

import React, { createContext, useContext, useReducer, useCallback } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  loading: boolean;
}

type AuthAction =
  | { type: 'AUTH_START' }
  | { type: 'AUTH_SUCCESS'; payload: User }
  | { type: 'AUTH_FAILURE' }
  | { type: 'LOGOUT' };

const authReducer = (state: AuthState, action: AuthAction): AuthState => {
  switch (action.type) {
    case 'AUTH_START':
      return { ...state, loading: true };
    case 'AUTH_SUCCESS':
      return {
        user: action.payload,
        isAuthenticated: true,
        loading: false
      };
    case 'AUTH_FAILURE':
      return {
        user: null,
        isAuthenticated: false,
        loading: false
      };
    case 'LOGOUT':
      return {
        user: null,
        isAuthenticated: false,
        loading: false
      };
    default:
      return state;
  }
};

interface AuthContextValue extends AuthState {
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

interface AuthProviderProps {
  children: React.ReactNode;
}

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, {
    user: null,
    isAuthenticated: false,
    loading: false
  });

  const login = useCallback(async (email: string, password: string) => {
    dispatch({ type: 'AUTH_START' });
    try {
      const user = await authService.login(email, password);
      dispatch({ type: 'AUTH_SUCCESS', payload: user });
    } catch (error) {
      dispatch({ type: 'AUTH_FAILURE' });
      throw error;
    }
  }, []);

  const logout = useCallback(() => {
    authService.logout();
    dispatch({ type: 'LOGOUT' });
  }, []);

  const value: AuthContextValue = {
    ...state,
    login,
    logout
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

Zustand for Complex State Management

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
  createdAt: Date;
}

interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  
  // Actions
  addTodo: (text: string) => void;
  toggleTodo: (id: string) => void;
  deleteTodo: (id: string) => void;
  setFilter: (filter: 'all' | 'active' | 'completed') => void;
  clearCompleted: () => void;
  
  // Selectors
  filteredTodos: () => Todo[];
  completedCount: () => number;
  activeCount: () => number;
}

export const useTodoStore = create<TodoState>()(
  devtools(
    persist(
      immer((set, get) => ({
        todos: [],
        filter: 'all',

        addTodo: (text: string) =>
          set((state) => {
            state.todos.push({
              id: crypto.randomUUID(),
              text,
              completed: false,
              createdAt: new Date()
            });
          }),

        toggleTodo: (id: string) =>
          set((state) => {
            const todo = state.todos.find(t => t.id === id);
            if (todo) {
              todo.completed = !todo.completed;
            }
          }),

        deleteTodo: (id: string) =>
          set((state) => {
            state.todos = state.todos.filter(t => t.id !== id);
          }),

        setFilter: (filter) =>
          set((state) => {
            state.filter = filter;
          }),

        clearCompleted: () =>
          set((state) => {
            state.todos = state.todos.filter(t => !t.completed);
          }),

        filteredTodos: () => {
          const { todos, filter } = get();
          switch (filter) {
            case 'active':
              return todos.filter(t => !t.completed);
            case 'completed':
              return todos.filter(t => t.completed);
            default:
              return todos;
          }
        },

        completedCount: () => get().todos.filter(t => t.completed).length,
        activeCount: () => get().todos.filter(t => !t.completed).length
      })),
      {
        name: 'todo-storage'
      }
    )
  )
);

// Usage in component
const TodoList: React.FC = () => {
  const { filteredTodos, toggleTodo, deleteTodo } = useTodoStore();
  const todos = filteredTodos();

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span className={todo.completed ? 'completed' : ''}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
};

Performance Optimization

Memoization and Optimization

import React, { memo, useMemo, useCallback, useState } from 'react';

interface ExpensiveComponentProps {
  items: Array<{ id: string; name: string; value: number }>;
  onItemClick: (id: string) => void;
}

// Memoize expensive calculations
const ExpensiveComponent: React.FC<ExpensiveComponentProps> = memo(({
  items,
  onItemClick
}) => {
  // Memoize expensive computations
  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return items.reduce((sum, item) => sum + item.value, 0);
  }, [items]);

  // Memoize sorted items
  const sortedItems = useMemo(() => {
    console.log('Sorting items...');
    return [...items].sort((a, b) => b.value - a.value);
  }, [items]);

  return (
    <div>
      <p>Total: {expensiveValue}</p>
      <ul>
        {sortedItems.map(item => (
          <ExpensiveListItem
            key={item.id}
            item={item}
            onClick={onItemClick}
          />
        ))}
      </ul>
    </div>
  );
});

interface ListItemProps {
  item: { id: string; name: string; value: number };
  onClick: (id: string) => void;
}

const ExpensiveListItem: React.FC<ListItemProps> = memo(({ item, onClick }) => {
  // Memoize the click handler to prevent re-renders
  const handleClick = useCallback(() => {
    onClick(item.id);
  }, [item.id, onClick]);

  return (
    <li onClick={handleClick}>
      {item.name}: {item.value}
    </li>
  );
});

// Parent component with optimized callbacks
const ParentComponent: React.FC = () => {
  const [items, setItems] = useState([
    { id: '1', name: 'Item 1', value: 100 },
    { id: '2', name: 'Item 2', value: 200 }
  ]);

  // Memoize callback to prevent child re-renders
  const handleItemClick = useCallback((id: string) => {
    console.log('Item clicked:', id);
    // Handle item click logic
  }, []);

  return (
    <ExpensiveComponent
      items={items}
      onItemClick={handleItemClick}
    />
  );
};

Code Splitting and Lazy Loading

import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';
import { LoadingSpinner } from './components/LoadingSpinner';

// Lazy load components
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => 
  import('./pages/Dashboard').then(module => ({
    default: module.Dashboard
  }))
);

// Lazy load with retry logic
const lazyWithRetry = (importFunc: () => Promise<any>) => {
  return lazy(() => 
    importFunc().catch(error => {
      console.error('Failed to load component, retrying...', error);
      // Retry logic
      return importFunc();
    })
  );
};

const Settings = lazyWithRetry(() => import('./pages/Settings'));

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <ErrorBoundary>
        <Suspense fallback={<LoadingSpinner />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/settings" element={<Settings />} />
          </Routes>
        </Suspense>
      </ErrorBoundary>
    </BrowserRouter>
  );
};

export default App;

Testing Best Practices

Component Testing with React Testing Library

import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import { UserCard } from './UserCard';

const mockUser = {
  id: '1',
  name: 'John Doe',
  email: 'john@example.com',
  isActive: true
};

describe('UserCard', () => {
  it('renders user information correctly', () => {
    render(<UserCard user={mockUser} />);
    
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('john@example.com')).toBeInTheDocument();
    expect(screen.getByText('Active')).toBeInTheDocument();
  });

  it('calls onUserUpdate when edit form is submitted', async () => {
    const user = userEvent.setup();
    const mockOnUpdate = vi.fn();
    
    render(
      <UserCard user={mockUser} onUserUpdate={mockOnUpdate} />
    );

    // Click edit button
    await user.click(screen.getByRole('button', { name: /edit/i }));

    // Update name field
    const nameInput = screen.getByDisplayValue('John Doe');
    await user.clear(nameInput);
    await user.type(nameInput, 'Jane Doe');

    // Submit form
    await user.click(screen.getByRole('button', { name: /save/i }));

    // Verify callback was called with updated user
    expect(mockOnUpdate).toHaveBeenCalledWith({
      ...mockUser,
      name: 'Jane Doe'
    });
  });

  it('handles loading state during async operations', async () => {
    const mockOnUpdate = vi.fn(() => 
      new Promise(resolve => setTimeout(resolve, 1000))
    );
    
    render(
      <UserCard user={mockUser} onUserUpdate={mockOnUpdate} />
    );

    const editButton = screen.getByRole('button', { name: /edit/i });
    await userEvent.click(editButton);

    const saveButton = screen.getByRole('button', { name: /save/i });
    await userEvent.click(saveButton);

    // Check loading state
    expect(screen.getByText(/saving/i)).toBeInTheDocument();
    expect(saveButton).toBeDisabled();

    // Wait for operation to complete
    await waitFor(() => {
      expect(screen.queryByText(/saving/i)).not.toBeInTheDocument();
    });
  });

  it('handles error states gracefully', async () => {
    const mockOnUpdate = vi.fn(() => 
      Promise.reject(new Error('Network error'))
    );
    
    render(
      <UserCard user={mockUser} onUserUpdate={mockOnUpdate} />
    );

    const editButton = screen.getByRole('button', { name: /edit/i });
    await userEvent.click(editButton);

    const saveButton = screen.getByRole('button', { name: /save/i });
    await userEvent.click(saveButton);

    // Check error message appears
    await waitFor(() => {
      expect(screen.getByText(/failed to update user/i)).toBeInTheDocument();
    });
  });
});

Custom Hook Testing

import { renderHook, act } from '@testing-library/react';
import { vi } from 'vitest';
import { useAsync } from './useAsync';

// Mock async function
const mockAsyncFunction = vi.fn();

describe('useAsync', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should handle successful async operation', async () => {
    const expectedData = { id: 1, name: 'Test' };
    mockAsyncFunction.mockResolvedValue(expectedData);

    const { result } = renderHook(() => 
      useAsync(mockAsyncFunction, [])
    );

    // Initial state
    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(null);

    // Wait for async operation to complete
    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    // Final state
    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(expectedData);
    expect(result.current.error).toBe(null);
  });

  it('should handle async operation failure', async () => {
    const expectedError = new Error('Test error');
    mockAsyncFunction.mockRejectedValue(expectedError);

    const { result } = renderHook(() => 
      useAsync(mockAsyncFunction, [])
    );

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toBe(null);
    expect(result.current.error).toEqual(expectedError);
  });

  it('should refetch data when dependencies change', async () => {
    const { result, rerender } = renderHook(
      ({ dep }) => useAsync(() => mockAsyncFunction(dep), [dep]),
      { initialProps: { dep: 'initial' } }
    );

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(mockAsyncFunction).toHaveBeenCalledWith('initial');

    // Change dependency
    rerender({ dep: 'updated' });

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(mockAsyncFunction).toHaveBeenCalledWith('updated');
    expect(mockAsyncFunction).toHaveBeenCalledTimes(2);
  });
});

Accessibility Best Practices

Semantic HTML and ARIA

import React, { useState, useRef, useEffect } from 'react';

interface DropdownProps {
  label: string;
  options: Array<{ value: string; label: string }>;
  value?: string;
  onChange: (value: string) => void;
}

export const AccessibleDropdown: React.FC<DropdownProps> = ({
  label,
  options,
  value,
  onChange
}) => {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const buttonRef = useRef<HTMLButtonElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  // Handle keyboard navigation
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (!isOpen) return;

      switch (e.key) {
        case 'ArrowDown':
          e.preventDefault();
          setActiveIndex(prev => 
            prev < options.length - 1 ? prev + 1 : 0
          );
          break;
        case 'ArrowUp':
          e.preventDefault();
          setActiveIndex(prev => 
            prev > 0 ? prev - 1 : options.length - 1
          );
          break;
        case 'Enter':
        case ' ':
          e.preventDefault();
          if (activeIndex >= 0) {
            onChange(options[activeIndex].value);
            setIsOpen(false);
          }
          break;
        case 'Escape':
          setIsOpen(false);
          buttonRef.current?.focus();
          break;
      }
    };

    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
      return () => document.removeEventListener('keydown', handleKeyDown);
    }
  }, [isOpen, activeIndex, options, onChange]);

  // Focus management
  useEffect(() => {
    if (isOpen && activeIndex >= 0) {
      const activeOption = listRef.current?.children[activeIndex] as HTMLElement;
      activeOption?.scrollIntoView({ block: 'nearest' });
    }
  }, [activeIndex, isOpen]);

  const selectedOption = options.find(opt => opt.value === value);

  return (
    <div className="dropdown">
      <label id="dropdown-label" className="dropdown-label">
        {label}
      </label>
      
      <button
        ref={buttonRef}
        type="button"
        className="dropdown-button"
        onClick={() => setIsOpen(!isOpen)}
        onBlur={(e) => {
          // Close dropdown if focus moves outside
          if (!e.currentTarget.contains(e.relatedTarget)) {
            setIsOpen(false);
          }
        }}
        aria-labelledby="dropdown-label"
        aria-expanded={isOpen}
        aria-haspopup="listbox"
      >
        {selectedOption?.label || 'Select an option'}
        <span aria-hidden="true"></span>
      </button>

      {isOpen && (
        <ul
          ref={listRef}
          className="dropdown-list"
          role="listbox"
          aria-labelledby="dropdown-label"
        >
          {options.map((option, index) => (
            <li
              key={option.value}
              className={`dropdown-option ${
                index === activeIndex ? 'active' : ''
              } ${option.value === value ? 'selected' : ''}`}
              role="option"
              aria-selected={option.value === value}
              onClick={() => {
                onChange(option.value);
                setIsOpen(false);
                buttonRef.current?.focus();
              }}
              onMouseEnter={() => setActiveIndex(index)}
            >
              {option.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

Build Configuration and Tooling

Vite Configuration

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [
    react({
      // Enable React Refresh
      fastRefresh: true,
      // Configure JSX runtime
      jsxRuntime: 'automatic'
    })
  ],
  
  // Path resolution
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
      '@components': resolve(__dirname, 'src/components'),
      '@hooks': resolve(__dirname, 'src/hooks'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@types': resolve(__dirname, 'src/types')
    }
  },

  // Development server
  server: {
    port: 3000,
    open: true,
    cors: true
  },

  // Build configuration
  build: {
    target: 'esnext',
    outDir: 'dist',
    sourcemap: true,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          utils: ['lodash', 'date-fns']
        }
      }
    }
  },

  // Environment variables
  define: {
    __APP_VERSION__: JSON.stringify(process.env.npm_package_version)
  }
});

ESLint Configuration

// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "plugins": [
    "react",
    "react-hooks",
    "@typescript-eslint",
    "jsx-a11y"
  ],
  "rules": {
    "react/react-in-jsx-scope": "off",
    "react/prop-types": "off",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/explicit-function-return-type": "off",
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn",
    "jsx-a11y/anchor-is-valid": "off"
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

This guide provides a comprehensive foundation for modern React development. Remember to:

  1. Use TypeScript for type safety and better developer experience
  2. Follow component composition patterns for reusable and maintainable code
  3. Optimize performance with memoization and code splitting
  4. Write comprehensive tests for components and hooks
  5. Ensure accessibility with semantic HTML and ARIA attributes
  6. Use modern tooling like Vite for fast development and builds
  7. Implement proper state management based on application complexity

Stay updated with React ecosystem changes and adopt new patterns and tools as they mature and become widely adopted by the community.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment