📋 Inhaltsverzeichnis
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
📚 Verwandte Artikel
TypeScript für React Entwickler
Erfahren Sie mehr über verwandte Webentwicklungs-Themen.
18 min LesezeitNext.js Performance Optimization
Erfahren Sie mehr über verwandte Webentwicklungs-Themen.
12 min LesezeitWeb Performance Monitoring
Erfahren Sie mehr über verwandte Webentwicklungs-Themen.
13 min Lesezeit