Tavo-IT Logo
Webentwicklung15 min Lesezeit2025-06-12

React Best Practices2025 Edition

Moderne React-Entwicklung mit Hooks, Context und Performance-Patterns – Best Practices für professionelle und skalierbare React-Anwendungen.

ReactHooksBest PracticesPerformanceTypeScript

⚛️ React Best Practices 2025

React hat sich seit der Einführung von Hooks drastisch weiterentwickelt. Die Best Practices von 2025 fokussieren auf funktionale Komponenten, moderne Hooks-Patterns und Performance-Optimierung. Dieser Guide zeigt dir die wichtigsten Techniken für professionelle React-Entwicklung.

  • Funktionale Komponenten: Hooks statt Class Components
  • Custom Hooks: Wiederverwendbare Logik abstrahieren
  • Performance: Memoization und Lazy Loading
  • TypeScript: Type Safety für bessere Entwicklung

🪝 Modern Hooks Patterns

Custom Hook für 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;
}

// Verwendung:
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 für komplexen 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 für globalen 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 };

// Verwendung:
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 })}
    </>
  );
}

// Verwendung:
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 Optimierung

React.memo für 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>
  );
}

// Verwendung:
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 Dateien für saubere Imports
  • Absolute Imports: Verwende tsconfig paths für saubere Imports
  • Component Co-location: Verwandte Dateien zusammen organisieren
  • Naming Conventions: Konsistente Benennung für bessere Lesbarkeit