Zarządzanie stanem za pomocą React Hooks

Wstęp

Na ostatniej konferencji React Conf 2018 zaprezentowano React Hooks – nową funkcjonalność mającą zrewolucjonizować podejście do tworzenia komponentów. Dan Abramov – współtwórca Reduxa i kontrybutor Reacta, powiedział, że kiedy cztery lata temu miał pierwszy raz styczność z Reactem zastanawiał się w jaki sposób logo odzwierciedla działanie tej biblioteki. Projekt przecież nie nazywa się Atom, nie jest silnikiem fizycznym. Atomy kojarzą się z reakcjami chemicznymi, może do tego twórcy loga Reacta chcieli nawiązać. Według jego interpretacji, która mi wydaje się bardziej sensowna – orbity w logo odzwierciedlają właśnie hooki, czyli małe funkcje pozwalające nam zarządzać całą naszą aplikacją, którą reprezentuję punkt pośrodku. Hooki dają nam dostęp do tego co już znamy, ale w zupełnie odmienny niż dotychczas sposób, który może być bezpośrednio utożsamiany ze sposobem w jaki działa React – czyli dzieleniem kodu na mniejsze niezależne komponenty.

Problem i rozwiązanie

Od dłuższego czasu społeczność wokół Reacta skarży się na problem związany z współdzieleniem logiki pomiędzy komponentami. Problem ten początkowo próbowano rozwiązać niesławnymi Mixinami, które jak się okazało powodowały więcej problemów niż pomagały dlatego zostały usunięte z biblioteki. React Hooks wydają się być bardziej przemyślaną formą rozwiązania tego problemu.

Idea

Hooki to funkcje dostarczone przez twórców Reacta, dzięki którym możemy manipulować stanem lokalnym komponentów nie będących klasami JavaScript. Pozwalają na dostęp do metod lifecycle, kontekstu aplikacji i wszystkich innych rzeczy, do których aby mieć dostęp musieliśmy rozszerzać nasz komponent/klasę o React.Component. Możemy zamknąć pewną logikę aplikacji związaną z modyfikacją stanu lub metodą lifecycle w funkcji i później używać jej wielokrotnie w dowolnym miejscu w aplikacji.

Korzyści

Testy komponentów funkcyjnych są prostsze do napisania i utrzymywania. Używanie funkcji zamiast klas powoduje mniejszą złożoność kodu co przekłada się na bardziej czytelny i prosty do napisania kod. Zwiększenie reużywalności kodu związanego bezpośrednio z API Reacta powoduję, że aplikację można napisać jeszcze szybciej.

Na npm możemy obserwować aktualnie wysyp paczek zawierających niestandardowe hooki, które dla przykładu umożliwiają, pobranie wymiarów okna przeglądarki i bindowanie go ze stanem lokalnym danego komponentu czy innych operacji na obiekcie window. Możemy takie niezależne wydzielone funkcje pobrać i zastosować w naszym kodzie, a hook sam wepnie się w potrzebne mu metody lifecycle danego komponentu, w którym został użyty.

Zarządzanie stanem

Zanim przejdziemy do kodu chciałbym zaznaczyć, że w tym artykule skupiam się tylko na hookach, które pozwalają nam na zarządzanie stanem. Jeśli interesują Cię inne hooki, to zapraszam do zapoznania się z nimi tutaj. Przejdźmy do kodu. Zaczniemy od podstawowego hooka – useState() pozwalającego ustawić nam stan lokalny w komponencie funkcyjnym oraz daje nam możliwość jego zmiany.

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
     <p>
       You clicked {count} times
     </p>
     <button onClick={() => setCount(count + 1)}>Click me</button>      
    </div>
  );
}

Aby lepiej pokazać różnicę pomiędzy nowym a “starym” podejściem, poniżej ten sam kod z użyciem „starego” podejścia klasowego.

class Example extends React.Component { 
    constructor(props) { 
        super(props); this.state = { count: 0 }; 
    } 
    render() { 
      return (
        <div>
          <p>You clicked {count} times </p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }
}

useReducer()

React hooks daje nam także możliwość implementacji architektury flux do zarządzania lokalnym stanem naszego komponentu. Należy pamiętać, że stan którym zarządzamy jest dostępny tylko w obrębie danego komponentu, nie należy mylić tego z Redux gdzie zarządzanym globalnym stanem. Myślę, że niejedna osoba rozmarzyła się nad wykorzystaniem tego mechanizmu dla stanu globalnego aplikacji. Co prawda React nie zaimplementował takiego hooka, ale w następnym punkcie przedstawię bibliotekę, która implementuje taki mechanizm.

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>

  );
}

Jak widać powyżej useReducer() zwracam nam stan komponentu oraz metodę dispatch, dzięki której możemy wysłać akcję do naszego reducera analogicznie jak w Redux.

Houx

Biblioteka wykorzystująca powyższy mechanizm do zarządzania stanem globalnym aplikacji. Doskonały przykład synergii pomiędzy najnowszymi możliwościami jakie daje na React.

Dzięki połączeniu React Hooks z React Context jesteśmy w stanie zaimplementować architekturę flux w naszej aplikacji korzystając jedynie z API Reacta. Houx daję wszystkie podstawowe możliwości, które znamy z Reduxa.

Poniżej prezentuję kod, który implementuję fluxa i dzieli przestrzeń globalną na podprzestrzenie dla poszczególnych reducerów.

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

const Context = createContext();

export const Provider = ({ store, children }) => (
   <context.Provider value={useReducer(store, store())}>
       {children}
   </context>
)

export function createStoreWithReducers(schema) {
   return (state, action) => {
       let combinedReducers = {};
       const schemaEntries = Object.entries(schema);
       schemaEntries.forEach((e) => {
           const [namespace, reducer] = e;
           combinedReducers[namespace] = reducer(state && state[namespace], action);
       })
       return combinedReducers;
   }
}

export const useHoux = () => useContext(Context);

Do Providera z React Context przekazujemy nasz nowy nabytek – useReducer(), do którego jako pierwszy parametr podajemy reducer podany przez użytkownika, następnie wywołujemy go w drugim parametrze aby wygenerować stan początkowy i przekazać go do naszego globalnego kontekstu.

Funkcja createStoreWithReducers jest swego rodzaju utilsem, który rozbija nam nasz główny reducer na parę mniejszych zgodnie ze schematem podanym przez użytkownika.

Na końcu tworzymy nasz customowy hook – useHoux, który po prostu wyciąga i wywołuje nasz useReducer() z kontekstu globalnego.

Powyższy kod został spakowany do paczki npm i jest dostępny z konsoli.

npm install --save houx

Dzięki Houx nie potrzebujemy zewnętrznych bibliotek aby zarządzać stanem naszej aplikacji a paczka sama w sobie jest tylko pluginem do Reacta. Można powiedzieć, że zarządzamy stanem w sposób natywny dla Reacta. Przy pomocy Houx wydaje się to jeszcze prostsze i przyjemniejsze.

Analogicznie jak w przypadku Reduxa ustawimy Provider jako komponent nadrzędny nad naszą aplikacją i przekazujemy mu nasze reducery aby stworzyć stan początkowy oraz umożliwić dostęp do stanu globalnego z każdego komponentu w aplikacji.

import React from 'react';
import { Provider, createStoreWithReducers } from 'houx';

const reducers = {
    tasks: tasksReducer
}

export default function App() {
  const store = createStoreWithReducers(reducers);
  return (
    <provider store={store}>
        <appcontent></appcontent>
    </provider>
  )
}
 

Następnie jeśli chcemy wysłać akcje do odpowiedniego reducera, odpalamy ją dispatchem, do którego uzyskamy dostęp za pomocą naszego hooka – useHoux(). Analogicznie jeśli chcielibyśmy wyciągnąć dane ze stanu globalnego.

import React from 'react';
import { useHoux } from 'houx';
import { removeTask } from '../../../flux/actions/tasksActions';

export default function RemoveButton(props) {
    const [state, dispatch] = useHoux();

    const onClick = () => {
        dispatch(removeTask(props.itemId));
    }

    return (
        <span className="cursor-pointer"
            style={{ marginLeft: '0.5em' }}
            onClick={onClick}
            role="img"
            aria-label="cross">x</span>
    )
}

Więcej kodu z użyciem React Hooks oraz pełny przykład wykorzystania Houx można znaleźć pod adresem: https://github.com/glaskawiec/houxTasks

Przyszłość Reduxa

Wiele osób podczas czytania tego artykułu pewnie zacznie zastanawiać się nad przyszłością Reduxa. W internecie pełno jest pytań i dyskusji stylu “React Hooks zabije Reduxa?” Moja odpowiedź na to pytanie brzmi “Nie, przynajmniej jeszcze nie teraz”. Redux ma całą masę innych funkcjonalności takich jak middleware czy akcje asynchroniczne. Nie zapomnijmy o toolach do debugowania, które są niezbędne przy pracy z większymi projektami. Pojawiły się hooki, które pełnią rolę “wcześniejszego” connecta Reduxowego co tylko potwierdza, że Redux ma się całkiem dobrze.

Podsumowanie

W dzisiejszych czasach React daje nam tak duże możliwości jeśli chodzi zarządzanie stanem aplikacji, że powinniśmy zastanowić się czy należy sięgać po zewnętrzne biblioteki ponieważ często okażą się one zbędne. Dzięki wprowadzeniu hooka – useReducer() zarządzanie skomplikowanym stanem stało się znacznie prostsze. Jeśli skorzystamy z mechanizmu React Context jesteśmy w stanie użyć wyżej wymienionego hooka na skalę globalną co daje nam podobne możliwości jak w Reduxie.

Software developer pracujący na co dzień ze stackiem React && Redux. Podążający za nowinkami technicznymi entuzjasta JavaScript. Wysoce reaktywny członek śląskiej społeczności IT. Chętnie dzieli się wiedzą i eksperymentami w kodzie. Po godzinach realizuje projekty jako freelancer, wolne chwilę spędza na implementacji własnych pomysłów i projektów.
PODZIEL SIĘ