React ewoluuje w zawrotnym tempie. W 2025 roku mamy Server Components, ulepszone Hooks API, nowe wzorce zarządzania stanem i wiele więcej. Oto 12 sprawdzonych praktyk, które stosuję w projektach produkcyjnych dla klientów.
Spis treści
- 1. React Server Components (RSC)
- 2. TypeScript - nie opcja, a standard
- 3. Custom Hooks patterns
- 4. Nowoczesne zarządzanie stanem
- 5. Error Boundaries + Suspense
- 6. Memoizacja (kiedy i jak)
- 7. Composition over Inheritance
- 8. Code Splitting & Lazy Loading
- 9. Testing Strategy
- 10. Performance Optimization
- 11. Accessibility (a11y)
- 12. Security Best Practices
1. React Server Components (RSC)
Server Components to największa zmiana w React od lat. Pozwalają renderować komponenty po stronie serwera bez wysyłania ich kodu do przeglądarki.
Kiedy używać Server Components?
- Fetching danych z bazy (bezpośrednie queries, bez API)
- Dostęp do secrets/env variables (nie trafiają do bundle)
- Heavy computations (nie blokują głównego wątku)
// app/products/page.tsx (Server Component - domyślnie w Next.js 14+)
async function ProductsPage() {
// Bezpośredni dostęp do DB - bez API layer
const products = await db.product.findMany();
return (
<div>
<h1>Produkty</h1>
{products.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
2. TypeScript - nie opcja, a standard
W 2025 TypeScript to must-have. Redukuje błędy runtime o ~40% i znacząco poprawia DX.
Type-safe Props
// ❌ Słabo
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
}
// ✅ Dobrze
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
Generic Components
// Type-safe lista dla dowolnego typu danych
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Użycie
<List
items={products}
renderItem={(p) => <ProductCard product={p} />}
keyExtractor={(p) => p.id}
/>
3. Custom Hooks Patterns
Custom Hooks to sposób na reużywalną logikę. Nazywaj je zawsze z prefiksem use.
Pattern: Data Fetching Hook
// hooks/useProduct.ts
import { useState, useEffect } from 'react';
interface UseProductReturn {
product: Product | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
export function useProduct(id: string): UseProductReturn {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchProduct = async () => {
try {
setLoading(true);
const res = await fetch(`/api/products/${id}`);
const data = await res.json();
setProduct(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProduct();
}, [id]);
return { product, loading, error, refetch: fetchProduct };
}
// Użycie
function ProductDetail({ id }: { id: string }) {
const { product, loading, error } = useProduct(id);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!product) return <NotFound />;
return <ProductCard product={product} />;
}
4. Nowoczesne zarządzanie stanem
Redux ma się dobrze, ale w 2025 są lepsze opcje dla większości projektów:
Zustand (lightweight, ~1KB)
// store/useCartStore.ts
import { create } from 'zustand';
interface CartState {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clear: () => void;
}
export const useCartStore = create<CartState>((set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
clear: () => set({ items: [] }),
}));
// Użycie
function Cart() {
const items = useCartStore((state) => state.items);
const removeItem = useCartStore((state) => state.removeItem);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>Usuń</button>
</div>
))}
</div>
);
}
- Local state (1-2 komponenty):
useState- Global state (cała app): Zustand / Jotai
- Server state (API data): React Query / SWR
- Complex workflows: Redux Toolkit
5. Error Boundaries + Suspense
Graceful error handling to must-have. Połącz Error Boundaries z Suspense:
// components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: (error: Error) => ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error!);
}
return this.props.children;
}
}
// Użycie
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<Suspense fallback={<LoadingSpinner />}>
<ProductList />
</Suspense>
</ErrorBoundary>
6. Memoizacja (kiedy i jak)
⚠️ Nie optymalizuj przedwcześnie! Memoizacja dodaje overhead. Używaj tylko gdy:
- Komponent renderuje się często z tymi samymi props
- Renderowanie jest kosztowne (heavy computations, duże listy)
- Profiler pokazuje problem
React.memo dla komponentów
// ❌ Bez sensu - prosty komponent
const Button = React.memo(({ label }) => <button>{label}</button>);
// ✅ Ma sens - ciężki komponent
const HeavyChart = React.memo(({ data }: { data: number[] }) => {
// Expensive rendering (np. 1000+ punktów na wykresie)
return <ComplexChart data={data} />;
});
useMemo dla wartości
function ProductList({ products, filter }: Props) {
// ✅ Memoizacja - filtrowanie może być kosztowne
const filteredProducts = useMemo(() => {
return products.filter(p => p.category === filter);
}, [products, filter]);
return filteredProducts.map(p => <ProductCard key={p.id} product={p} />);
}
7. Composition over Inheritance
React faworyzuje kompozycję. Zamiast dziedziczenia, używaj children i render props.
Pattern: Compound Components
// components/Card.tsx
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>;
}
function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>;
}
// Export jako namespace
Card.Header = CardHeader;
Card.Body = CardBody;
export { Card };
// Użycie
<Card>
<Card.Header>Tytuł</Card.Header>
<Card.Body>Treść karty...</Card.Body>
</Card>
8. Code Splitting & Lazy Loading
Nie ładuj całej aplikacji od razu. Next.js robi to automatycznie, ale możesz ręcznie:
import { lazy, Suspense } from 'react';
// Dynamic import - ładowane dopiero gdy potrzebne
const AdminPanel = lazy(() => import('./AdminPanel'));
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showAdmin, setShowAdmin] = useState(false);
return (
<div>
<h1>Dashboard</h1>
{showAdmin && (
<Suspense fallback={<div>Ładowanie panelu...</div>}>
<AdminPanel />
</Suspense>
)}
</div>
);
}
9. Testing Strategy
Testing Pyramid: Unit tests (70%) → Integration tests (20%) → E2E tests (10%)
Vitest + Testing Library
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders label correctly', () => {
render(<Button label="Click me" onClick={() => {}} />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button label="Click" onClick={handleClick} />);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button label="Click" onClick={() => {}} disabled />);
expect(screen.getByRole('button')).toBeDisabled();
});
});
10. Performance Optimization
- Virtualizacja długich list:
react-window/react-virtualized - Debounce search inputs: Użyj
useDeferredValue(React 18+) - Optimistic updates: Update UI przed odpowiedzią API
- Image optimization: Next.js Image component z lazy loading
// useDeferredValue - built-in debouncing
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const results = useSearch(deferredQuery); // Nie wali API przy każdym keystroke
return <ResultsList results={results} />;
}
11. Accessibility (a11y)
Accessibility to nie opcja. 15% użytkowników używa assistive technologies.
Checklist
- ✅ Semantic HTML (
<button>nie<div onClick>) - ✅ ARIA labels gdzie potrzeba (
aria-label,aria-describedby) - ✅ Keyboard navigation (Tab, Enter, Escape)
- ✅ Focus management (focus trapping w modalach)
- ✅ Color contrast min. 4.5:1 (WCAG AA)
// ✅ Accessible modal
function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
// Focus trap - focus pierwszego elementu
modalRef.current?.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
>
<h2 id="modal-title">Tytuł modala</h2>
{children}
<button onClick={onClose} aria-label="Zamknij modal">✕</button>
</div>
);
}
12. Security Best Practices
- XSS prevention: React escapuje automatycznie, ale uważaj na
dangerouslySetInnerHTML - CSRF tokens: W formularzach do mutacji danych
- Secrets w env variables: Nigdy w kodzie (
NEXT_PUBLIC_*tylko dla publicznych) - Dependencies audit:
npm auditregularnie
// ❌ NIGDY
const apiKey = "sk_live_abc123"; // w kodzie!
// ✅ ZAWSZE
const apiKey = process.env.STRIPE_SECRET_KEY; // w .env (nie commituj!)
// Publiczne klucze (safe w bundle)
const publicKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
Podsumowanie
React w 2025 to ekosystem pełen możliwości. Kluczowe takeaways:
- Server Components tam gdzie możliwe (zero JS overhead)
- TypeScript zawsze (type safety = mniej bugów)
- Custom Hooks dla reużywalnej logiki
- Testing jako część developmentu, nie "potem"
- Accessibility od początku, nie na końcu
Pytania? Napisz w komentarzach lub skontaktuj się — chętnie pomogę!
Podobał Ci się artykuł?
Zapisz się do newslettera — raz w miesiącu dostaniesz podobne treści + ekskluzywne case studies.
Zapisz się do newslettera