Polski

Zanurz się w hooku useReducer w React, aby efektywnie zarządzać złożonymi stanami aplikacji, poprawiając wydajność i łatwość utrzymania globalnych projektów.

Wzorzec useReducer w React: Opanowanie złożonego zarządzania stanem

W stale ewoluującym krajobrazie rozwoju front-endu, React ugruntował swoją pozycję jako wiodący framework do budowania interfejsów użytkownika. W miarę wzrostu złożoności aplikacji, zarządzanie stanem staje się coraz większym wyzwaniem. Hook useState zapewnia prosty sposób na zarządzanie stanem wewnątrz komponentu, ale w bardziej skomplikowanych scenariuszach React oferuje potężną alternatywę: hook useReducer. Ten wpis na blogu zagłębia się we wzorzec useReducer, badając jego korzyści, praktyczne implementacje oraz to, jak może on znacząco ulepszyć Twoje aplikacje React na skalę globalną.

Zrozumienie potrzeby złożonego zarządzania stanem

Podczas tworzenia aplikacji w React często napotykamy sytuacje, w których stan komponentu nie jest jedynie prostą wartością, ale raczej zbiorem wzajemnie powiązanych danych lub stanem, który zależy od poprzednich wartości stanu. Rozważmy następujące przykłady:

W tych scenariuszach używanie samego useState może prowadzić do skomplikowanego i trudnego do zarządzania kodu. Aktualizowanie wielu zmiennych stanu w odpowiedzi na jedno zdarzenie może stać się uciążliwe, a logika zarządzania tymi aktualizacjami może być rozproszona po całym komponencie, co utrudnia jej zrozumienie i utrzymanie. To właśnie tutaj useReducer pokazuje swoją siłę.

Wprowadzenie do hooka useReducer

Hook useReducer jest alternatywą dla useState do zarządzania złożoną logiką stanu. Opiera się na zasadach wzorca Redux, ale jest zaimplementowany w samym komponencie React, co w wielu przypadkach eliminuje potrzebę stosowania oddzielnej, zewnętrznej biblioteki. Pozwala na scentralizowanie logiki aktualizacji stanu w jednej funkcji zwanej reducerem.

Hook useReducer przyjmuje dwa argumenty:

Hook zwraca tablicę zawierającą dwa elementy:

Funkcja Reducera

Funkcja reducera jest sercem wzorca useReducer. Jest to czysta funkcja, co oznacza, że nie powinna mieć żadnych efektów ubocznych (takich jak wywoływanie API czy modyfikowanie zmiennych globalnych) i zawsze powinna zwracać ten sam wynik dla tych samych danych wejściowych. Funkcja reducera przyjmuje dwa argumenty:

Wewnątrz funkcji reducera używa się instrukcji switch lub if/else if do obsługi różnych typów akcji i odpowiedniej aktualizacji stanu. Centralizuje to logikę aktualizacji stanu i ułatwia zrozumienie, jak stan zmienia się w odpowiedzi na różne zdarzenia.

Funkcja Dispatch

Funkcja dispatch to metoda, której używasz do wywoływania aktualizacji stanu. Gdy wywołujesz dispatch(action), akcja jest przekazywana do funkcji reducera, która następnie aktualizuje stan na podstawie typu i ładunku (payload) akcji.

Praktyczny przykład: implementacja licznika

Zacznijmy od prostego przykładu: komponentu licznika. Ilustruje on podstawowe koncepcje, zanim przejdziemy do bardziej złożonych przykładów. Stworzymy licznik, który można inkrementować, dekrementować i resetować:


import React, { useReducer } from 'react';

// Definiowanie typów akcji
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

// Definiowanie funkcji reducera
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case RESET:
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  // Inicjalizacja useReducer
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Licznik: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Zwiększ</button>
      <button onClick={() => dispatch({ type: DECREMENT })}>Zmniejsz</button>
      <button onClick={() => dispatch({ type: RESET })}>Resetuj</button>
    </div>
  );
}

export default Counter;

W tym przykładzie:

Rozszerzenie przykładu z licznikiem: dodanie ładunku (payload)

Zmodyfikujmy licznik, aby umożliwić inkrementację o określoną wartość. Wprowadza to koncepcję ładunku (payload) w akcji:


import React, { useReducer } from 'react';

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';

function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + action.payload };
    case DECREMENT:
      return { count: state.count - action.payload };
    case RESET:
      return { count: 0 };
    case SET_VALUE:
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  const [inputValue, setInputValue] = React.useState(1);

  return (
    <div>
      <p>Licznik: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT, payload: parseInt(inputValue) || 1 })}>Zwiększ o {inputValue}</button>
      <button onClick={() => dispatch({ type: DECREMENT, payload: parseInt(inputValue) || 1 })}>Zmniejsz o {inputValue}</button>
      <button onClick={() => dispatch({ type: RESET })}>Resetuj</button>
       <input
         type="number"
         value={inputValue}
         onChange={(e) => setInputValue(e.target.value)}
       />
      </div>
  );
}

export default Counter;

W tym rozszerzonym przykładzie:

Korzyści z używania useReducer

Wzorzec useReducer oferuje kilka zalet w porównaniu z bezpośrednim użyciem useState do złożonego zarządzania stanem:

Kiedy używać useReducer

Chociaż useReducer oferuje znaczące korzyści, nie zawsze jest to właściwy wybór. Rozważ użycie useReducer, gdy:

W przypadku prostych aktualizacji stanu useState jest często wystarczający i prostszy w użyciu. Podejmując decyzję, weź pod uwagę złożoność swojego stanu i potencjał jego rozwoju.

Zaawansowane koncepcje i techniki

Łączenie useReducer z Context

Do zarządzania stanem globalnym lub udostępniania stanu między wieloma komponentami można połączyć useReducer z Context API Reacta. To podejście jest często preferowane zamiast Redux w małych i średnich projektach, w których nie chcesz wprowadzać dodatkowych zależności.


import React, { createContext, useReducer, useContext } from 'react';

// Definiowanie typów akcji i reducera (jak wcześniej)
const INCREMENT = 'INCREMENT';
// ... (inne typy akcji i funkcja counterReducer)

const CounterContext = createContext();

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

function useCounter() {
  return useContext(CounterContext);
}

function Counter() {
  const { state, dispatch } = useCounter();

  return (
    <div>
      <p>Licznik: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Zwiększ</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

export default App;

W tym przykładzie:

Testowanie useReducer

Testowanie reducerów jest proste, ponieważ są to czyste funkcje. Można łatwo przetestować funkcję reducera w izolacji, używając frameworka do testów jednostkowych, takiego jak Jest lub Mocha. Oto przykład z użyciem Jesta:


import { counterReducer } from './counterReducer'; // Zakładając, że counterReducer jest w osobnym pliku

const INCREMENT = 'INCREMENT';

descripe('counterReducer', () => {
  it('powinien inkrementować licznik', () => {
    const state = { count: 0 };
    const action = { type: INCREMENT };
    const newState = counterReducer(state, action);
    expect(newState.count).toBe(1);
  });

   it('powinien zwrócić ten sam stan dla nieznanych typów akcji', () => {
        const state = { count: 10 };
        const action = { type: 'UNKNOWN_ACTION' };
        const newState = counterReducer(state, action);
        expect(newState).toBe(state); // Sprawdzenie, czy stan się nie zmienił
    });
});

Testowanie reducerów zapewnia, że zachowują się one zgodnie z oczekiwaniami i ułatwia refaktoryzację logiki stanu. Jest to kluczowy krok w budowaniu solidnych i łatwych w utrzymaniu aplikacji.

Optymalizacja wydajności za pomocą memoizacji

Podczas pracy ze złożonymi stanami i częstymi aktualizacjami, rozważ użycie useMemo do optymalizacji wydajności komponentów, zwłaszcza jeśli masz wartości pochodne obliczane na podstawie stanu. Na przykład:


import React, { useReducer, useMemo } from 'react';

function reducer(state, action) {
  // ... (logika reducera)
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // Obliczanie wartości pochodnej, memoizując ją za pomocą useMemo
  const derivedValue = useMemo(() => {
    // Kosztowne obliczenia na podstawie stanu
    return state.value1 + state.value2;
  }, [state.value1, state.value2]); // Zależności: przeliczaj tylko, gdy te wartości się zmienią

  return (
    <div>
      <p>Wartość pochodna: {derivedValue}</p>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE1', payload: 10 })}>Aktualizuj wartość 1</button>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE2', payload: 20 })}>Aktualizuj wartość 2</button>
    </div>
  );
}

W tym przykładzie derivedValue jest obliczana tylko wtedy, gdy zmieni się state.value1 lub state.value2, co zapobiega niepotrzebnym obliczeniom przy każdym ponownym renderowaniu. To podejście jest powszechną praktyką w celu zapewnienia optymalnej wydajności renderowania.

Praktyczne przykłady i przypadki użycia

Przyjrzyjmy się kilku praktycznym przykładom, w których useReducer jest cennym narzędziem do tworzenia aplikacji React dla globalnej publiczności. Zauważ, że te przykłady są uproszczone, aby zilustrować podstawowe koncepcje. Rzeczywiste implementacje mogą obejmować bardziej złożoną logikę i zależności.

1. Filtry produktów w e-commerce

Wyobraź sobie witrynę e-commerce (pomyśl o popularnych platformach, takich jak Amazon czy AliExpress, dostępnych na całym świecie) z dużym katalogiem produktów. Użytkownicy muszą filtrować produkty według różnych kryteriów (zakres cen, marka, rozmiar, kolor, kraj pochodzenia itp.). useReducer jest idealny do zarządzania stanem filtrów.


import React, { useReducer } from 'react';

const initialState = {
  priceRange: { min: 0, max: 1000 },
  brand: [], // Tablica wybranych marek
  color: [], // Tablica wybranych kolorów
  //... inne kryteria filtrowania
};

function filterReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_PRICE_RANGE':
      return { ...state, priceRange: action.payload };
    case 'TOGGLE_BRAND':
      const brand = action.payload;
      return { ...state, brand: state.brand.includes(brand) ? state.brand.filter(b => b !== brand) : [...state.brand, brand] };
    case 'TOGGLE_COLOR':
      // Podobna logika dla filtrowania kolorów
      return { ...state, color: state.color.includes(action.payload) ? state.color.filter(c => c !== action.payload) : [...state.color, action.payload] };
    // ... inne akcje filtrów
    default:
      return state;
  }
}

function ProductFilter() {
  const [state, dispatch] = useReducer(filterReducer, initialState);

  // Komponenty UI do wybierania kryteriów filtrowania i wywoływania akcji dispatch
  // Na przykład: suwak dla ceny, pola wyboru dla marek itp.

  return (
    <div>
      <!-- Elementy UI filtrów -->
    </div>
  );
}

Ten przykład pokazuje, jak obsługiwać wiele kryteriów filtrowania w kontrolowany sposób. Gdy użytkownik modyfikuje dowolne ustawienie filtra (cenę, markę itp.), reducer odpowiednio aktualizuje stan filtra. Komponent odpowiedzialny za wyświetlanie produktów używa następnie zaktualizowanego stanu do filtrowania wyświetlanych produktów. Ten wzorzec wspiera budowanie złożonych systemów filtrowania, powszechnych na globalnych platformach e-commerce.

2. Formularze wieloetapowe (np. międzynarodowe formularze wysyłkowe)

Wiele aplikacji zawiera formularze wieloetapowe, takie jak te używane do międzynarodowej wysyłki lub tworzenia kont użytkowników o złożonych wymaganiach. useReducer doskonale radzi sobie z zarządzaniem stanem takich formularzy.


import React, { useReducer } from 'react';

const initialState = {
  step: 1, // Bieżący krok w formularzu
  formData: {
    firstName: '',
    lastName: '',
    address: '',
    city: '',
    country: '',
    // ... inne pola formularza
  },
  errors: {},
};

function formReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return { ...state, formData: { ...state.formData, [action.payload.field]: action.payload.value } };
    case 'SET_ERRORS':
      return { ...state, errors: action.payload };
    case 'SUBMIT_FORM':
      // Obsługa logiki wysyłania formularza tutaj, np. wywołania API
      return state;
    default:
      return state;
  }
}

function MultiStepForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  // Logika renderowania dla każdego kroku formularza
  // Na podstawie bieżącego kroku w stanie
  const renderStep = () => {
    switch (state.step) {
      case 1:
        return <Step1 formData={state.formData} dispatch={dispatch} />;
      case 2:
        return <Step2 formData={state.formData} dispatch={dispatch} />;
      // ... inne kroki
      default:
        return <p>Nieprawidłowy krok</p>;
    }
  };

  return (
    <div>
      {renderStep()}
      <!-- Przyciski nawigacyjne (Dalej, Wstecz, Wyślij) na podstawie bieżącego kroku -->
    </div>
  );
}

To ilustruje, jak zarządzać różnymi polami formularza, krokami i potencjalnymi błędami walidacji w sposób ustrukturyzowany i łatwy w utrzymaniu. Jest to kluczowe dla budowania przyjaznych dla użytkownika procesów rejestracji lub finalizacji zamówienia, zwłaszcza dla użytkowników międzynarodowych, którzy mogą mieć różne oczekiwania w oparciu o swoje lokalne zwyczaje i doświadczenia z różnymi platformami, takimi jak Facebook czy WeChat.

3. Aplikacje czasu rzeczywistego (czat, narzędzia do współpracy)

useReducer jest korzystny w aplikacjach czasu rzeczywistego, takich jak narzędzia do współpracy typu Google Docs czy aplikacje do przesyłania wiadomości. Obsługuje zdarzenia takie jak odbieranie wiadomości, dołączanie/opuszczanie czatu przez użytkownika oraz status połączenia, zapewniając, że interfejs użytkownika jest aktualizowany w razie potrzeby.


import React, { useReducer, useEffect } from 'react';

const initialState = {
  messages: [],
  users: [],
  connectionStatus: 'connecting',
};

function chatReducer(state, action) {
  switch (action.type) {
    case 'RECEIVE_MESSAGE':
      return { ...state, messages: [...state.messages, action.payload] };
    case 'USER_JOINED':
      return { ...state, users: [...state.users, action.payload] };
    case 'USER_LEFT':
      return { ...state, users: state.users.filter(user => user.id !== action.payload.id) };
    case 'SET_CONNECTION_STATUS':
      return { ...state, connectionStatus: action.payload };
    default:
      return state;
  }
}

function ChatRoom() {
  const [state, dispatch] = useReducer(chatReducer, initialState);

  useEffect(() => {
    // Ustanowienie połączenia WebSocket (przykład):
    const socket = new WebSocket('wss://your-websocket-server.com');

    socket.onopen = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
    socket.onmessage = (event) => dispatch({ type: 'RECEIVE_MESSAGE', payload: JSON.parse(event.data) });
    socket.onclose = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });

    return () => socket.close(); // Czyszczenie przy odmontowywaniu komponentu
  }, []);

  // Renderowanie wiadomości, listy użytkowników i statusu połączenia na podstawie stanu
  return (
    <div>
      <p>Status połączenia: {state.connectionStatus}</p>
      <!-- UI do wyświetlania wiadomości, listy użytkowników i wysyłania wiadomości -->
    </div>
  );
}

Ten przykład stanowi podstawę do zarządzania czatem w czasie rzeczywistym. Stan przechowuje wiadomości, użytkowników aktualnie na czacie oraz status połączenia. Hook useEffect jest odpowiedzialny za ustanowienie połączenia WebSocket i obsługę przychodzących wiadomości. Takie podejście tworzy responsywny i dynamiczny interfejs użytkownika, który zaspokaja potrzeby użytkowników na całym świecie.

Najlepsze praktyki używania useReducer

Aby efektywnie używać useReducer i tworzyć łatwe w utrzymaniu aplikacje, rozważ następujące najlepsze praktyki:

Podsumowanie

Hook useReducer jest potężnym i wszechstronnym narzędziem do zarządzania złożonym stanem w aplikacjach React. Oferuje liczne korzyści, w tym scentralizowaną logikę stanu, lepszą organizację kodu i zwiększoną testowalność. Stosując najlepsze praktyki i rozumiejąc jego podstawowe koncepcje, możesz wykorzystać useReducer do budowania bardziej solidnych, łatwych w utrzymaniu i wydajnych aplikacji React. Ten wzorzec pozwala skutecznie radzić sobie z wyzwaniami związanymi ze złożonym zarządzaniem stanem, umożliwiając tworzenie aplikacji gotowych na globalny rynek, które zapewniają płynne doświadczenia użytkownikom na całym świecie.

W miarę zagłębiania się w rozwój React, włączenie wzorca useReducer do swojego zestawu narzędzi niewątpliwie doprowadzi do czystszych, bardziej skalowalnych i łatwiejszych w utrzymaniu baz kodu. Pamiętaj, aby zawsze brać pod uwagę specyficzne potrzeby swojej aplikacji i wybierać najlepsze podejście do zarządzania stanem w każdej sytuacji. Miłego kodowania!