useEffect w React - z czym to się je?

Hooki jako natywne API Reacta znacząco odmieniło podejście do tego frameworka. Zyskał on na piękności oraz sprawił, że nie potrzebujemy już używać komponentów klasowych. Jednym z najczęściej używanych hooków jest useEffect, czyli funkcja odpowiadająca w dużej mierze za operacje w cyklu życia komponentu. Z useEffect jednak wiążą się pewne smaczki. Jakie?

Hooka useEffect używamy do wprawnego operowania na zmiennych i często zaawansowanych rzeczy związanych z logiką komponentu. Często korzystamy z niego do integracji z innymi hookami jak useState, czy refami. Liczba przydatności tego hooka jest naprawdę duża, a przykładowymi mogą być: zaciąganie danych z API po zamantowaniu komponentu, przypisanie do stanu wartości z propsów, czy też dodawanie zdarzeń do elementów DOM (a raczej JSX) w naszym komponencie. To właśnie na zdarzeniu opiszę zagadnienie jakim jest useEffect.

useEffect w praktyce

Rozważmy sobie prosty komponent renderujący button w aplikacji reactowej:

//App.js
import React from 'react';

const ParentComponent = () => {
  return <button type='button'>Click me!</button>;
};

export default ParentComponent;

Powiedzmy, że chcemy kliknąć button i zczytać jego zawartość, ale z jakiegoś powodu chcemy użyć postawowe API DOMu w JavaScripcie a nie propsa onClick. Aby to zrobić, importuję hooka useRef do referencji buttona i spróbujmy go sobie od razu wyświetlić w konsoli:

import React, { useRef } from 'react';

const ParentComponent = () => {
  const buttonElement = useRef(null);
  console.log(buttonElement);                     //wyświetlenie buttonElement

  return (
    <button type='button' ref={buttonElement}>
      Click me!
    </button>
  );
};

export default ParentComponent;
zdjęcie tematyczne
zwrócona wartość będzie wynosiła null

W ten sposób nie wyciągniemy wartości z refa. Dzieje się tak ponieważ przed wyrenderowaniem JSXa przez komponent przypisujemy do niego wartość inicjacyjną (czyli null), a zaraz po tym próbujemy go wyświetlić. Ponadto propsa ref do renderowanego komponentu wysyłamy dopiero wtedy, gdy komponent funkcyjny zwraca dany JSX. Dlatego do zmiennej referencyjnej zostanie przypisana zmienna dopiero po wyrenderowaniu. Jednak wtedy nie przyda nam się on na wiele.

Aby poradzić sobie z tym problemem użyjemy useEffect i to w nim wyświetlimy wartość referencyjną do buttona:

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

const ParentComponent = () => {
  const buttonElement = useRef(null);

  useEffect(() => {
    console.log(buttonElement);
  });

  return (
    <button type='button' ref={buttonElement}>
      Click me!
    </button>
  );
};

export default ParentComponent;
zdjęcie tematyczne
Teraz za każdym wyrenderowaniem komponentu widoczna będzie wartość refu

Do otrzymanego buttonu chcemy dodać zdarzenie kliknięcia, by otrzymać wartość tekstową buttona. Zróbmy to także w useEffect:

//...
useEffect(() => {
    buttonElement.current.addEventListener('click', (e) => {
      const { textContent } = e.target;
      console.log(textContent);
    });
  });
//...

Dzięki temu otrzymamy wartość przycisku, czyli "Click me!".

No dobrze, osiągneliśmy swój cel, ale nie można zostawić tego zdarzenia samemu sobie. Nie możemy, ponieważ w momencie gdy komponent się wymontuje (odmontuje), a dzieje się tak ciężko, to zdarzenie będzie nadal przypisane do elmentu. Często jest to powodem poważnych błędów w aplikacji. A zatem usuńmy zdarzenie. Zrobimy to za pomocą zwracanej wartości w funkcji w useEffect:

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

const ParentComponent = () => {
  const buttonElement = useRef(null);

  useEffect(() => {
    buttonElement.current.addEventListener('click', getTextContent);

    return () => {
      buttonElement.current.removeEventListener('click', getTextContent);
    };
  });

  const getTextContent = (e) => {
    const { textContent } = e.target;
    console.log(textContent);
  };

  return (
    <button type='button' ref={buttonElement}>
      Click me!
    </button>
  );
};

export default ParentComponent;

W addEventListener i removeEventListener uzyłem przepisanej funkcji, by nie powtarzać tej samej funkcji wyświetlającej wartość buttona. Dzięki powyższemu zabiegowi zabezpieczymy komponent i nasze zdarzenie będzie za każdym razem usuwane, gdy komponent zniknie z projektu. funkcja zwracająca w useEffect jest odpowiednikiem componentWillUnmount dla komponentów klasowych.

Druga wartość jako tablica w useEffect

W hooku useEffect używamy dwóch argumentów: pierwszy to oczywiście funkcja wykonująca daną czynność i drugi - opcjonalny - tablicę ze zmiennymi. Jeżeli do drugiego argumentu wrzucimy pustą tablicę, to komponent będzie się renderował tylko jeden jedyny raz, ponieważ wewnątrz tablicy nie podamy żadnej zmiennej na podstawie, której komponent się zmieni. Jeżeli chcemy rerenderować komponent za każdym razem kiedy zmieni się wartość jakiegoś propsa, np. name lub jakiejkolwiek zmiennej wewnątrz komponentu, to dodajemy do tablicy tego propsa lub zmienną. Jest to bardzo przydatna rzecz, dzięki której możemy dawać instrukcje dla komponentów, kiedy mają się renderować, a co za tym idzie, kiedy ma odpalać się np. @keyframes lub kiedy zaciągamy nowe dane z API na miejsce starych.

Przykład użycia pustej tablicy jako drugiego argumentu dla useEffect (w tym przypadku "Renderowanie komponentu" wyświetli się tylko raz - przy pierwszym renderowaniu):

import React, { useEffect } from 'react';

const ParentComponent = () => {
  useEffect(() => {
    console.log('Renderowanie komponentu');
  }, []);
//...

Przykład użycia tablicy z dwoma elementami jako drugiego argumentu dla useEffect (w tym przypadku komponent zmieni, czyli wyrenderuje się, za każdym razem kiedy propsy firstName i lastName zmienią swoją wartość):

import React, { useEffect } from 'react';

const ParentComponent = ({firstName, lastName}) => {
  useEffect(() => {
    console.log('Renderowanie komponentu');
  }, [firstName, lastName]);
//...
© Damian Kalka 2021
Wszelkie prawa zastrzeżone