React / Best Practices

React Best Practices 2025 - Nowoczesny Development

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.

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>
  );
}
💡 Tip: Server Components = zero JavaScript w bundle. Client Components (z "use client") tylko tam, gdzie potrzebujesz interaktywności.

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>
  );
}
💡 Rule of thumb:
- 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 audit regularnie
// ❌ 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