Tavo-IT Logo
Web Development15 min read2025-06-12

React Best Practices2025 Edition

Modern React development with Hooks, Context, and performance patterns – best practices for professional and scalable React applications.

ReactHooksBest PracticesPerformanceTypeScript

⚛️ React Best Practices 2025

React has evolved dramatically since the introduction of Hooks. The best practices of 2025 focus on functional components, modern hooks patterns, and performance optimization. This guide shows you the most important techniques for professional React development.

  • Functional Components: Hooks instead of class components
  • Custom Hooks: Abstract reusable logic
  • Performance: Memoization and lazy loading
  • TypeScript: Type safety for better development

🪝 Modern Hooks Patterns

Custom hook for API calls:

// hooks/useApi.ts
import { useState, useEffect } from 'react';

interface UseApiState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

export function useApi<T>(url: string): UseApiState<T> {
  const [state, setState] = useState<UseApiState<T>>({
    data: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        setState(prev => ({ ...prev, loading: true, error: null }));
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        setState({ data, loading: false, error: null });
      } catch (error) {
        setState({ 
          data: null, 
          loading: false, 
          error: error instanceof Error ? error.message : 'Unknown error' 
        });
      }
    };

    fetchData();
  }, [url]);

  return state;
}

// Usage:
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return <div>{user.name}</div>;
}

useLocalStorage Hook:

// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}

📊 State Management

useReducer for complex state:

// types/shopping-cart.ts
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  total: number;
}

type CartAction =
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: string }
  | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
  | { type: 'CLEAR_CART' };

// hooks/useShoppingCart.ts
import { useReducer } from 'react';

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find(item => item.id === action.payload.id);
      
      if (existingItem) {
        const updatedItems = state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
        return {
          items: updatedItems,
          total: calculateTotal(updatedItems)
        };
      }
      
      const newItems = [...state.items, { ...action.payload, quantity: 1 }];
      return {
        items: newItems,
        total: calculateTotal(newItems)
      };
    }
    
    case 'REMOVE_ITEM': {
      const filteredItems = state.items.filter(item => item.id !== action.payload);
      return {
        items: filteredItems,
        total: calculateTotal(filteredItems)
      };
    }
    
    case 'UPDATE_QUANTITY': {
      const updatedItems = state.items.map(item =>
        item.id === action.payload.id
          ? { ...item, quantity: action.payload.quantity }
          : item
      );
      return {
        items: updatedItems,
        total: calculateTotal(updatedItems)
      };
    }
    
    case 'CLEAR_CART':
      return { items: [], total: 0 };
      
    default:
      return state;
  }
}

export function useShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
  
  return {
    ...state,
    addItem: (item: Omit<CartItem, 'quantity'>) => 
      dispatch({ type: 'ADD_ITEM', payload: item }),
    removeItem: (id: string) => 
      dispatch({ type: 'REMOVE_ITEM', payload: id }),
    updateQuantity: (id: string, quantity: number) => 
      dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }),
    clearCart: () => dispatch({ type: 'CLEAR_CART' })
  };
}

Context for global state:

// context/ThemeContext.tsx
import { createContext, useContext, ReactNode } from 'react';

interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

🧩 Component Patterns

Compound Components Pattern:

// components/Modal/Modal.tsx
import { createContext, useContext, ReactNode, useState } from 'react';

interface ModalContextType {
  isOpen: boolean;
  open: () => void;
  close: () => void;
}

const ModalContext = createContext<ModalContextType | undefined>(undefined);

function Modal({ children }: { children: ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  const open = () => setIsOpen(true);
  const close = () => setIsOpen(false);

  return (
    <ModalContext.Provider value={{ isOpen, open, close }}>
      {children}
    </ModalContext.Provider>
  );
}

function ModalTrigger({ children }: { children: ReactNode }) {
  const context = useContext(ModalContext);
  if (!context) throw new Error('ModalTrigger must be used within Modal');

  return (
    <button onClick={context.open}>
      {children}
    </button>
  );
}

function ModalContent({ children }: { children: ReactNode }) {
  const context = useContext(ModalContext);
  if (!context) throw new Error('ModalContent must be used within Modal');

  if (!context.isOpen) return null;

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white p-6 rounded-lg max-w-md w-full mx-4">
        {children}
      </div>
    </div>
  );
}

function ModalClose({ children }: { children: ReactNode }) {
  const context = useContext(ModalContext);
  if (!context) throw new Error('ModalClose must be used within Modal');

  return (
    <button onClick={context.close}>
      {children}
    </button>
  );
}

// Export compound component
Modal.Trigger = ModalTrigger;
Modal.Content = ModalContent;
Modal.Close = ModalClose;

export { Modal };

// Usage:
function App() {
  return (
    <Modal>
      <Modal.Trigger>
        Open Modal
      </Modal.Trigger>
      <Modal.Content>
        <h2>Modal Title</h2>
        <p>Modal content goes here...</p>
        <Modal.Close>
          Close
        </Modal.Close>
      </Modal.Content>
    </Modal>
  );
}

Render Props Pattern:

// components/DataFetcher.tsx
interface DataFetcherProps<T> {
  url: string;
  children: (data: {
    data: T | null;
    loading: boolean;
    error: string | null;
    refetch: () => void;
  }) => ReactNode;
}

export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
  const { data, loading, error } = useApi<T>(url);
  const [refetchKey, setRefetchKey] = useState(0);

  const refetch = () => setRefetchKey(prev => prev + 1);

  return (
    <>
      {children({ data, loading, error, refetch })}
    </>
  );
}

// Usage:
function UserList() {
  return (
    <DataFetcher<User[]> url="/api/users">
      {({ data: users, loading, error, refetch }) => {
        if (loading) return <div>Loading users...</div>;
        if (error) return <div>Error: {error}</div>;
        
        return (
          <div>
            <button onClick={refetch}>Refresh</button>
            {users?.map(user => (
              <div key={user.id}>{user.name}</div>
            ))}
          </div>
        );
      }}
    </DataFetcher>
  );
}

Performance optimization

React.memo for Component Memoization:

// components/UserCard.tsx
import { memo } from 'react';

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

interface UserCardProps {
  user: User;
  onEdit: (id: string) => void;
}

// Memoize component to prevent unnecessary re-renders
export const UserCard = memo(function UserCard({ user, onEdit }: UserCardProps) {
  console.log(`Rendering UserCard for ${user.name}`);
  
  return (
    <div className="border p-4 rounded">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>
        Edit
      </button>
    </div>
  );
});

// Custom comparison function für komplexere Vergleiche
export const UserCardWithCustomComparison = memo(
  function UserCard({ user, onEdit }: UserCardProps) {
    return (
      <div className="border p-4 rounded">
        <h3>{user.name}</h3>
        <p>{user.email}</p>
        <button onClick={() => onEdit(user.id)}>Edit</button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Nur re-rendern wenn sich user.id oder user.name ändert
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name &&
      prevProps.onEdit === nextProps.onEdit
    );
  }
);

useMemo und useCallback:

// components/ProductList.tsx
import { useMemo, useCallback, useState } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

function ProductList({ products }: { products: Product[] }) {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');

  // Memoize expensive calculations
  const filteredAndSortedProducts = useMemo(() => {
    console.log('Filtering and sorting products...');
    
    const filtered = products.filter(product =>
      product.name.toLowerCase().includes(searchTerm.toLowerCase())
    );

    return filtered.sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name);
      }
      return a.price - b.price;
    });
  }, [products, searchTerm, sortBy]);

  // Memoize callback functions
  const handleSearch = useCallback((term: string) => {
    setSearchTerm(term);
  }, []);

  const handleSort = useCallback((sortOption: 'name' | 'price') => {
    setSortBy(sortOption);
  }, []);

  // Memoize component statistics
  const stats = useMemo(() => {
    const totalProducts = filteredAndSortedProducts.length;
    const averagePrice = totalProducts > 0
      ? filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) / totalProducts
      : 0;

    return { totalProducts, averagePrice };
  }, [filteredAndSortedProducts]);

  return (
    <div>
      <SearchInput onSearch={handleSearch} />
      <SortButtons onSort={handleSort} currentSort={sortBy} />
      
      <div className="stats">
        <p>Products: {stats.totalProducts}</p>
        <p>Average Price: ${stats.averagePrice.toFixed(2)}</p>
      </div>
      
      <div className="product-grid">
        {filteredAndSortedProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

🛡️ Error Handling

Error Boundary:

// components/ErrorBoundary.tsx
import { Component, ReactNode, ErrorInfo } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    
    // Send error to logging service
    // logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Something went wrong!</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.stack}</pre>
          </details>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Hook-basierte Alternative für funktionale Komponenten
export function useErrorHandler() {
  return (error: Error, errorInfo?: { componentStack: string }) => {
    console.error('Error:', error);
    console.error('Component stack:', errorInfo?.componentStack);
    
    // Report to error tracking service
    // reportError(error, errorInfo);
  };
}

🧪 Testing best practices

React Testing Library:

// __tests__/UserCard.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from '../components/UserCard';

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

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

  it('calls onEdit when edit button is clicked', () => {
    const mockOnEdit = jest.fn();
    
    render(<UserCard user={mockUser} onEdit={mockOnEdit} />);
    
    fireEvent.click(screen.getByText('Edit'));
    
    expect(mockOnEdit).toHaveBeenCalledWith('1');
  });
});

// Hook Testing
import { renderHook, act } from '@testing-library/react';
import { useLocalStorage } from '../hooks/useLocalStorage';

describe('useLocalStorage', () => {
  beforeEach(() => {
    localStorage.clear();
  });

  it('returns initial value when no stored value exists', () => {
    const { result } = renderHook(() => 
      useLocalStorage('test-key', 'initial-value')
    );
    
    expect(result.current[0]).toBe('initial-value');
  });

  it('updates localStorage when value changes', () => {
    const { result } = renderHook(() => 
      useLocalStorage('test-key', 'initial')
    );
    
    act(() => {
      result.current[1]('updated');
    });
    
    expect(result.current[0]).toBe('updated');
    expect(localStorage.getItem('test-key')).toBe('"updated"');
  });
});

📝 TypeScript Integration

Generische Komponenten:

// components/List.tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

export function List<T>({
  items,
  renderItem,
  keyExtractor,
  emptyMessage = 'No items found'
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty-state">{emptyMessage}</div>;
  }

  return (
    <div className="list">
      {items.map((item, index) => (
        <div key={keyExtractor(item)} className="list-item">
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}

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

function UserList({ users }: { users: User[] }) {
  return (
    <List
      items={users}
      keyExtractor={(user) => user.id}
      renderItem={(user) => (
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
      emptyMessage="No users found"
    />
  );
}

📁 Code Organisation

Empfohlene Ordnerstruktur:

src/
├── components/           # Wiederverwendbare UI-Komponenten
│   ├── ui/              # Basis UI-Komponenten (Button, Input, etc.)
│   ├── forms/           # Formular-Komponenten
│   └── layout/          # Layout-Komponenten
├── pages/               # Page-Komponenten
├── hooks/               # Custom Hooks
├── context/             # React Context Providers
├── utils/               # Utility-Funktionen
├── types/               # TypeScript Type Definitionen
├── services/            # API Services
├── constants/           # Konstanten
└── __tests__/           # Test-Dateien

best practices:

  • Ein Export pro Datei: Klare und einfache Imports
  • Barrel exports: index.ts files for clean imports
  • Absolute imports: Use tsconfig paths for clean imports
  • Component Co-location: Verwandte Dateien zusammen organisieren
  • Naming conventions: Consistent naming for better readability