25 Najtrudniejszych pytań z React Hook

Początkowo React używał głównie komponentów klasowych, co było męczące. React hooks pozwalają teraz robić to wszystko w komponentach funkcyjnych. Zobacz najciekawsze pytania i odpowiedzi o React Hooks, które pomogą Ci w rozmowie kwalifikacyjnej.

Co to są React Hooks?

React Hooks to funkcjonalność wprowadzona w React 16.8, która umożliwia korzystanie ze stanu (state) i innych cech Reacta bez konieczności tworzenia komponentów klasowych. Dzięki Hooks możesz:

  • Wydzielić logikę odpowiadającą zarządzaniu stanem z komponentu, co umożliwia jej niezależne testowanie i ponowne wykorzystanie.
  • Używać dedykowanych funkcji do zarządzania stanem i efektami ubocznymi w komponentach funkcyjnych.
  • Utrzymać hierarchię komponentów bez modyfikowania jej struktury, co ułatwia ponowne wykorzystanie logiki stanu zarówno w obrębie jednego projektu, jak i w szerszej społeczności.

Ten sposób podejścia znacznie upraszcza zarządzanie logiką aplikacji, eliminując potrzebę korzystania z bardziej złożonych rozwiązań, takich jak HOC (Higher-Order Components) czy render props.

Przydatne materiały:


Jak uzyskać dostęp do elementów DOM w React?

Jedną z przydatnych zastosowań hooka useRef() jest umożliwienie dostępu do elementów DOM w komponencie. Proces ten przebiega w trzech krokach:

  1. Definicja referencji: Utwórz referencję, która będzie służyć do identyfikacji elementu, np.
    const elementRef = useRef();

  2. Przypisanie referencji do elementu: Przekaż referencję do atrybutu ref wybranego elementu, np.
    <div ref={elementRef}></div>

  3. Odwołanie się do elementu: Po zamontowaniu komponentu, wartość elementRef.current będzie wskazywać na docelowy element DOM.

Poniższy przykład ilustruje opisany mechanizm:

import {useRef, useEffect} from 'react';

function AccessingElement() {
    const elementRef = useRef();

    useEffect(() => {
        const divElement = elementRef.current;
        console.log(divElement); // wyświetli: <div>I'm an element</div>
    }, []);

    return (
        <div ref={elementRef}>
            I'm an element
        </div>
    );
}

Dla pogłębienia wiedzy oraz uzyskania dodatkowych informacji, warto zajrzeć do oficjalnej dokumentacji React dotyczącej Refs and the DOM. Polecamy również artykuły na temat zaawansowanych zagadnień związanych z React Hooks, takie jak 27 Advanced React Hooks Interview Questions – publikacja ta może stanowić wartościowe źródło wiedzy dla programistów pracujących z React.


Jak wywołać funkcję ładowania przy użyciu hooka Reacta useEffect tylko raz, tj. po początkowym renderowaniu komponentu?

Aby uruchomić funkcję ładowania wyłącznie podczas montowania komponentu (czyli po pierwszym renderowaniu), należy przekazać do hooka useEffect drugi argument – pustą tablicę []. Dzięki temu efekt zostanie wykonany tylko raz, gdy komponent zostanie zamontowany. Oto przykładowy fragment kodu:

function MyComponent() {
    useEffect(() => {
        loadDataOnlyOnce(); // Funkcja wywoływana tylko raz
    }, []); // Pusta tablica jako drugi argument gwarantuje jednokrotne wywołanie efektu

    return <div>{ /* Reszta komponentu */}</div>;
}

Dodatkowe materiały:


Poniżej znajduje się przykładowe tłumaczenie pytania wraz z odpowiedzią na język polski, przy użyciu profesjonalnego języka programistycznego i IT.


Podaj przykład prostego niestandardowego (custom) React Hooka.

Niestandardowy Hook w React to funkcja posiadająca stan, która wykorzystuje wbudowane Hooki (np. useState, useCallback itd.). Umożliwia on skupienie logiki związanej ze stanem w jednym miejscu, co pozwala uniknąć powielania tego samego kodu w różnych komponentach.

Przykładowo, rozważmy Hook odpowiadający za inkrementację i dekremetację:

import {useState} from 'react';

const useCounter = () => {
    const [counter, setCounter] = useState(0);

    return {
        counter, // wartość countera
        increment: () => setCounter(counter + 1), // funkcja inkrementująca
        decrement: () => setCounter(counter - 1)  // funkcja dekrementująca
    };
};

Następnie, możemy go wykorzystać w komponencie w następujący sposób:

import React from 'react';

const Component = () => {
    const {counter, increment, decrement} = useCounter();

    return (
        <div>
            <span onClick={decrement}>-</span>
            <span style={{padding: "10px"}}>{counter}</span>
            <span onClick={increment}>+</span>
        </div>
    );
};

export default Component;

Dlaczego potrzebujemy niestandardowych Hooków?

Niestandardowe Hooki pozwalają na:

  • Reużywalność logiki: Dzięki nim możliwe jest wyodrębnienie logiki stanu niezależnie od struktury komponentu, co sprzyja ponownemu użyciu tego samego kodu w różnych częściach aplikacji.
  • Redukcję duplikacji: Zamiast kopiować i wklejać tę samą logikę w wielu komponentach, mamy możliwość umieszczenia jej w jednym miejscu.
  • Lepszą organizację kodu: Pozwalają skondensować funkcjonalność w mniejszych, łatwiejszych do zarządzania fragmentach, co znacząco ułatwia utrzymanie i rozwój kodu.

Przydatne materiały:


Czym jest useState() w React? Wyjaśnij, do czego służy wywołanie useState(0) w poniższym kodzie:

...
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState(...);
...
const setCount = () => {
    setCounter(count + 1);
    setMoreStuff(...);
...
};

useState to jeden z wbudowanych hooków w React. Wywołanie useState(0) zwraca tablicę (tuple), której pierwszy element (count) reprezentuje bieżący stan, a drugi (setCounter) to funkcja umożliwiająca aktualizację tego stanu.

Korzystając z funkcji setCounter, możemy w dowolnym miejscu aplikacji modyfikować wartość stanu count – w powyższym przykładzie jest to realizowane w funkcji setCount, gdzie obok aktualizacji stanu licznika wykonywane są inne operacje. Główną ideą korzystania z hooków jest możliwość pisania bardziej funkcyjnego kodu i unikanie wykorzystywania komponentów klasowych, jeżeli nie ma takiej konieczności.

Materiały pomocnicze:


Czy można inicjować stan (state) w React za pomocą funkcji? Podaj przykład.

Tak! Poniższy przykład ilustruje inicjalizację stanu przy użyciu funkcji przekazywanej do hooka useState:

const StateFromFn = () => {
  const [token] = useState(() => {
    const storedToken = window.localStorage.getItem("my-token");
    return storedToken || "default#-token#";
  });

  return <div>Token is {token}</div>;
};

W powyższym przykładzie funkcja przekazywana do useState jest wykonywana tylko podczas początkowej inicjalizacji stanu, co umożliwia np. pobranie tokena z localStorage i ustawienie wartości początkowej.

Materiały pomocnicze


Czy dwa komponenty korzystające z tego samego Hooka dzielą stan?

Nie. Niestandardowe Hooki (custom Hooks) to mechanizm umożliwiający ponowne wykorzystanie logiki stanowej (np. konfiguracji subskrypcji czy zapamiętywania aktualnej wartości). Jednak przy każdym użyciu niestandardowego Hooka cały stan oraz efekty zadeklarowane w jego obrębie są całkowicie izolowane.

Dodatkowe materiały


Wyjaśnij różnicę między hookami useState i useRef.

  1. Aktualizacja referencji utworzonej przez useRef nie powoduje ponownego renderowania komponentu, podczas gdy zmiana stanu za pomocą setState wyzwala ponowne renderowanie.
  2. useRef zwraca obiekt z własnością current, która przechowuje rzeczywistą wartość, podczas gdy useState zwraca tablicę z dwoma elementami – wartością stanu oraz metodą do jego aktualizacji.
  3. Właściwość current zwróconego obiektu przez useRef jest mutowalna, natomiast zmienna stanu uzyskana z * useState* jest niemutowalna.
  4. Aktualizacja referencji (poprzez useRef) odbywa się synchronicznie – zaktualizowana wartość jest dostępna od razu, podczas gdy aktualizacja stanu (poprzez useState) jest asynchroniczna – stan zostaje zaktualizowany po kolejnym renderowaniu komponentu.

Przykład użycia useRef (bez wywoływania renderowania):

const countRef = useRef(0);

const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
};

Przykład użycia useState (trigger re-render):

const [count, setCount] = useState(0);

const handle = () => {
    const updatedCount = count + 1;
    console.log(`Clicked ${updatedCount} times`);
    setCount(updatedCount);
};

Dodatkowe materiały:


Jak mogę wykorzystać Error Boundaries w funkcjonalnych komponentach Reacta?

Od wersji 16.2.0 nie istnieje możliwość, aby funkcjonalny komponent pełnił rolę error boundary. Metoda componentDidCatch() działa analogicznie do bloku catch {} w JavaScript, jednakże jest wykorzystywana w kontekście komponentów. Tylko komponenty klasowe mogą być error boundaries. W praktyce zazwyczaj definiuje się komponent error boundary raz i stosuje się go w całej aplikacji.

Należy również pamiętać, że bloki try/catch nie pokrywają wszystkich przypadków. Jeżeli komponent znajdujący się głęboko w hierarchii spróbuje zaktualizować swój stan i wystąpi błąd, blok try/catch w komponencie nadrzędnym nie zadziała – ponieważ aktualizacja nie odbywa się synchronicznie razem z komponentem potomnym.

Warto zwrócić uwagę, że istnieje kilka bibliotek dostępnych na npm, które implementują hooki umożliwiające obsługę error boundaries w funkcjonalnych komponentach.

Dodatkowe materiały:

  • Oficjalna dokumentacja Reacta – Error Boundaries
  • Artykuł: Effective Error Handling in React Applications – przegląd strategii obsługi błędów w React.
  • Książka: React – Up & Running (autor: Stoyan Stefanov), która omawia różne podejścia do budowania aplikacji w React wraz z obsługą błędów.

Jak używać componentWillMount() w React Hooks?

Nie można korzystać z istniejących metod cyklu życia (takich jak componentDidMount, componentDidUpdate, componentWillUnmount itd.) w hookach – są one dostępne wyłącznie w komponentach klasowych. W hookach używamy funkcji useEffect, która łączy w sobie funkcjonalności componentDidMount, componentDidUpdate oraz componentWillUnmount w jednym.

  1. Kod z componentDidMount
    Aby wykonać kod tylko podczas montowania komponentu, użyj hooka useEffect wraz z pustą tablicą zależności:
useEffect(() => {
    // Twój kod tutaj
}, []);
  1. Aktualizacja przy każdorazowym renderowaniu
    Jeśli nie podamy drugiego parametru (tablicy zależności), hook useEffect zostanie wywołany przy każdym renderowaniu komponentu (podobnie do componentDidUpdate), co może prowadzić do nieoczekiwanych konsekwencji:
useEffect(() => {
    // Twój kod tutaj
});
  1. Obsługa czyszczenia, analogiczna do componentWillUnmount
    Aby zrealizować logikę wywoływaną przy odmontowywaniu komponentu, należy zwrócić funkcję czyszczącą z hooka useEffect:
useEffect(() => {
    window.addEventListener('mousemove', () => {
    });

    // Funkcja zwracana zostanie wywołana przy odmontowaniu komponentu
    return () => {
        window.removeEventListener('mousemove', () => {
        });
    };
}, []);

Materiały referencyjne i przydatne źródła:


Jakie są typowe przypadki użycia useMemo?

Głównym celem hooka useMemo jest optymalizacja wydajności. Dzięki niemu możemy:

  • Uzyskać zmemoizowaną wartość,
  • Przekazać dwa argumenty: funkcję tworzącą (ang. create function), która zwraca wartość do zmemoizowania, oraz tablicę zależności (dependency array). Wartość zostanie przeliczona ponownie jedynie, gdy przynajmniej jeden z elementów tej tablicy ulegnie zmianie.

Stosowanie useMemo pozwala na:

  • Zachowanie równości referencyjnej wartości (co umożliwia przekazywanie ich jako właściwości do komponentów i potencjalne zapobieganie niepotrzebnym ponownym renderowaniom),
  • Eliminację wielokrotnego wykonywania operacji obciążających obliczeniowo, gdy parametry wejściowe pozostają bez zmian.

Przykład użycia

function App() {
    const [data, setData] = useState([/* ... dane ... */]);

    function format() {
        console.log('formatowanie...'); // komunikat pojawi się wyłącznie, gdy zmieni się "data"
        const formattedData = [];
        data.forEach(item => {
            const newItem = /* ... operacje na elemencie ... */;
            if (newItem) {
                formattedData.push(newItem);
            }
        });
        return formattedData;
    }

    // Zmemoizowana przetworzona wartość, przeliczana tylko przy zmianie "data"
    const formattedData = useMemo(format, [data]);

    return (
        <>
            {formattedData.map(item => (
                <div key={item.id}>
                    {item.title}
                </div>
            ))}
        </>
    );
}

Dodatkowe materiały i przydatne linki


Jakie są różnice między React.memo() a useMemo()?

  • React.memo() jest wyższego rzędu komponentem (HOC), który można użyć do opakowania komponentów, aby zapobiec ich ponownemu renderowaniu, chyba że zmienią się przekazywane do nich propsy.
    Przykłady oraz dokumentację znajdziesz w oficjalnej dokumentacji React.

  • useMemo() to hook Reacta, którego można użyć do opakowania funkcji wewnątrz komponentu. Dzięki niemu można upewnić się, że wyniki obliczeń zawartych w tej funkcji będą ponownie przeliczane tylko wtedy, gdy zmieni się jedna z zależności.
    Więcej szczegółów dotyczących hooka znajdziesz w dokumentacji React Hooks.


Jakie są przykłady użycia hooka useRef?

  • useRef zwraca prosty obiekt JavaScript (ang. plain Javascript object). Jego wartość można tyle razy odczytywać i modyfikować (mutowalność), ile potrzeba, bez konieczności martwienia się o ponowne renderowanie.
  • Wartość zwracana przez useRef utrzymuje się (nie resetuje się do wartości początkowej, w przeciwieństwie do zwykłego obiektu zadeklarowanego wewnątrz funkcji komponentu; utrzymuje się, ponieważ useRef zwraca ten sam obiekt zamiast tworzyć nowy przy kolejnych renderach) przez cały okres życia komponentu oraz przy kolejnych renderach.
  • Hook useRef jest często stosowany do przechowywania wartości zamiast referencji do elementów DOM. Mogą to być stany, które nie wymagają częstych zmian, bądź stany, które zmieniają się niezwykle często, ale nie powinny powodować pełnego ponownego renderowania komponentu.

Przykład użycia:

const refObject = useRef(initialValue);

Dodatkowe informacje na temat hooka useRef znajdziesz w dokumentacji React.


Jaki jest odpowiednik poniższego kodu przy użyciu React Hooks?

Załóżmy, że w naszym projekcie mamy metodę componentWillUnmount wykorzystywaną do sprzątania (np. usuwania nasłuchiwaczy zdarzeń, anulowania timerów itp.). Jak można przekształcić ten kod przy użyciu React Hooks?

Kod w komponencie klasowym:

componentDidMount()
{
    window.addEventListener('mousemove', () => {
    });
}

componentWillUnmount()
{
    window.removeEventListener('mousemove', () => {
    });
}

Odpowiednik z wykorzystaniem React Hooks przy użyciu hooka useEffect:

useEffect(() => {
    window.addEventListener('mousemove', () => {
    });

    // Funkcja zwracana zostanie wywołana przy odmontowaniu komponentu
    return () => {
        window.removeEventListener('mousemove', () => {
        });
    };
}, []);

Więcej informacji o hooku useEffect znajdziesz w oficjalnej dokumentacji React.


Kiedy stosować hook useContext?

Hook useContext w React ułatwia przekazywanie danych w całej aplikacji, bez konieczności ręcznego przekazywania propsów w dół hierarchii komponentów. React Context stanowi sposób na globalne zarządzanie stanem.

Przykładowe użycie:

import {useState, createContext} from "react";
import ReactDOM from "react-dom/client";

const UserContext = createContext();

Następnie opakowujemy komponenty potomne w Provider Context, dostarczając wartość stanu:

function Component1() {
    const [user, setUser] = useState("Jesse Hall");

    return (
        <UserContext.Provider value={user}>
            <h1>{`Hello ${user}!`}</h1>
            <Component2 user={user}/>
        </UserContext.Provider>
    );
}

Dostęp do wartości Context w innych komponentach:

import {useContext} from "react";

function Component5() {
    const user = useContext(UserContext);

    return (
        <>
            <h1>Component 5</h1>
            <h2>{`Hello ${user} again!`}</h2>
        </>
    );
}

Dalsze informacje na temat wykorzystania Contextu w React znajdziesz w dokumentacji React Context.


Kiedy zastosować useRef?

Główne zastosowania tego hooka to:

  1. Przechowywanie referencji do elementów DOM
    Używamy useRef do przechowywania referencji, dzięki czemu mamy możliwość późniejszego manipulowania danym elementem (np. ustawiania fokusu).

    Przykład:

function TextInputWithFocusButton() {
    const inputEl = useRef(null);
    const onButtonClick = () => {
        inputEl.current.focus();
    };
    return (
        <>
            <input ref={inputEl} type="text"/>
            <button onClick={onButtonClick}>Focus the input</button>
        </>
    );
}

Dodatkowe materiały:

  1. Przechowywanie wartości bez wywoływania ponownych renderów
    useRef może służyć do przechowywania zmiennych, które nie powinny wywoływać ponownego renderowania komponentu, np. przechowywanie poprzedniej wartości stanu.

    Przykład:

function Counter() {
    const [count, setCount] = useState(0);

    const prevCountRef = useRef();
    useEffect(() => {
        prevCountRef.current = count;
    });
    const prevCount = prevCountRef.current;

    return <h1>Obecnie: {count}, wcześniej: {prevCount}</h1>;
}

Dodatkowe artykuły:


Jaka jest różnica między Custom Hook a zwykłą funkcją?

Custom Hook to funkcja, która wykorzystuje wewnętrznie inne wbudowane hooki (np. useState, useEffect), dzięki czemu posiada stateful closure utrzymywane przez React, co pozwala na zarządzanie stanem związanym z konkretną instancją komponentu. Główne różnice to:

  • Stanowość:
    • Custom Hook będzie "stateful" tylko wtedy, gdy użyje hooka useState (lub innego implementującego mechanizm stanu) wewnątrz siebie.
  • Ograniczenie wywołań:
    • Hooki powinny być wywoływane wyłącznie z kodu React (np. wewnątrz komponentów funkcyjnych) i nie mogą być używane w zwykłych funkcjach JavaScript.
  • Dostępność hooków:
    • W komponentach klasowych nie są dostępne mechanizmy hooków, dlatego zwykłe funkcje mogą być tam użyte zamiast custom hooków.
    • W zwykłych funkcjach nie można uzyskać dostępu do useState, useEffect, useContext itp. – w przeciwieństwie do custom hooków, które umożliwiają wykorzystanie tych mechanizmów w kontekście React.

Dodatkowe zasoby:


Które metody cyklu życia komponentu klasowego są zastępowane przez useEffect w komponentach funkcyjnych?

Hook useEffect w komponentach funkcyjnych zastępuje następujące metody cyklu życia:

  • componentDidMount – odpowiada za wykonanie efektu jednokrotnie po zamontowaniu komponentu.

    Przykład:

useEffect(() => {
    console.log("To jest odpowiednik componentDidMount w useEffect");
}, []); // pusta tablica oznacza wywołanie efektu tylko przy montowaniu
  • componentDidUpdate – odpowiada za wykonywanie efektu po każdej zmianie określonych zależności.

    Przykład:

useEffect(() => {
    console.log("Właściwość props.name uległa zmianie!");
}, [props.name]);
  • componentWillUnmount – umożliwia wykonywanie czynności przed odmontowaniem komponentu, poprzez zwrócenie funkcji porządkowej (cleanup function).

    Przykład:

useEffect(() => {
    console.log('Uruchomienie efektu');
    return () => {
        console.log('Komponent zostanie odmontowany');
    };
});

Dodatkowe lektury:


Czy Hooki zastępują render props oraz higher-order components (HOC)?

Często komponenty wykorzystujące render props oraz higher-order components renderują tylko jeden element. Zespół Reacta uważa, że Hooki stanowią prostsze rozwiązanie dla tego przypadku użycia.

Oba podejścia nadal mają swoje zastosowanie – na przykład, komponent odpowiadający za wirtualne przewijanie (virtual scroller) może korzystać z prop-u renderItem, lub komponent wizualnego kontenera może posiadać własną strukturę DOM. Jednakże, w większości scenariuszy Hooki wystarczą, a korzystanie z nich może również pomóc w ograniczeniu głębokości zagnieżdżeń w drzewie komponentów.

Przydatne materiały:
Oficjalna dokumentacja React Hooks
Render props – dokumentacja React
Higher-Order Components – dokumentacja React


Jak zaktualizować stan w zagnieżdżonym obiekcie przy użyciu useState()?

Rozważmy komponent, który otrzymuje właściwość (prop) o następującej strukturze:

const styles = {
    font: {
        size: {
            value: '22',
            unit: 'px'
        },
        weight: 'bold',
        color: '#663300',
        family: 'arial',
        align: 'center'
    }
};

Jak zaktualizować tylko właściwość align?

Aby zaktualizować jedynie pojedynczą właściwość w zagnieżdżonym obiekcie, należy użyć operatora spread. Dodatkowo, przy aktualizacji stanu na podstawie poprzedniego stanu, warto zastosować wzorzec callback w funkcji setState:

const {...styling} = styles;
const [style, setStyle] = useState(styling);

setStyle(prevStyle => ({
    ...prevStyle,
    font: {...prevStyle.font, align: event.target.value}
}));

Przydatne materiały:
Dokumentacja useState – React
Operator spread w JavaScript – MDN


Jak ograniczyć wielokrotne renderowanie komponentu przy użyciu wielu wywołań useState?

Rozważmy poniższy przykładowy fragment kodu:

const getData = url => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(async () => {
        const test = await api.get('/people');

        if (test.ok) {
            setLoading(false);
            setData(test.data.results);
        }
    }, []);

    return {data, loading};
};

W przypadku pobrania danych oraz aktualizacji dwóch oddzielnych zmiennych stanu (data i flagi loading), komponent ( np. tabela danych) jest renderowany dwukrotnie – mimo że obie aktualizacje stanu występują wewnątrz tej samej funkcji. Istnieje kilka sposobów na ograniczenie tej sytuacji:

  1. Scal stan loading i data w jeden obiekt stanu:
    Dzięki temu możliwe jest wykonanie pojedynczego wywołania funkcji setState, co spowoduje tylko jeden cykl renderowania.
const [userRequest, setUserRequest] = useState({
    loading: false,
    user: null,
});

useEffect(() => {
    // Uwaga: wywołanie poniżej zastępuje cały obiekt, więc klucz user zostanie usunięty!
    setUserRequest({loading: true});
    fetch('https://randomuser.me/api/')
        .then(results => results.json())
        .then(data => {
            setUserRequest({
                loading: false,
                user: data.results[0],
            });
        });
}, []);
  1. Grupowanie aktualizacji stanu w React przed wersją 18:
    Przed React 18, grupowanie (batching) aktualizacji stanu odbywa się tylko wtedy, gdy są one wywoływane z wewnątrz event handlerów React, takich jak kliknięcie przycisku lub zmiana wartości input. Aktualizacje wywołane poza takim handlerem, np. asynchroniczne wywołania, nie są automatycznie grupowane.

  2. Automatyczne grupowanie aktualizacji od React 18:
    Od wersji React 18 (funkcja dostępna na zasadzie opt-in) wszystkie aktualizacje stanu są automatycznie grupowane, niezależnie od miejsca ich wywołania.

Przydatne materiały:
Nowości w React 18 – blog Reacta
Wprowadzenie do Hooków i useState – dokumentacja React


Podaj dobry przykład użycia hooka useCallback w React

Wyobraź sobie, że masz komponent renderujący dużą listę elementów. Aby zapobiec niepotrzebnym renderowaniom listy, owijamy go funkcją React.memo().

import useSearch from './fetch-items';

function MyBigList({term, onItemClick}) {
    const items = useSearch(term);
    const map = item => <div onClick={onItemClick}>{item}</div>;
    return <div>{items.map(map)}</div>;
}

export default React.memo(MyBigList);

Komponent nadrzędny względem MyBigList przekazuje funkcję obsługi zdarzeń, aby wiedzieć, kiedy element został kliknięty:

import {useCallback} from 'react';

export function MyParent({term}) {
    const onItemClick = useCallback(event => {
        console.log('Kliknięto ', event.currentTarget);
    }, [term]);

    return (
        <MyBigList
            term={term}
            onItemClick={onItemClick}
        />
    );
}

Funkcja onItemClick jest memoizowana za pomocą hooka useCallback(). Dopóki wartość term się nie zmienia, useCallback() zwraca ten sam obiekt funkcji, który jest przekazywany do MyBigList jako właściwość (prop). W momencie, gdy komponent MyParent ponownie renderuje się, obiekt funkcji onItemClick pozostaje niezmieniony i * nie narusza memoizacji* komponentu MyBigList. Jest to prawidłowy przypadek zastosowania hooka useCallback.

Więcej informacji możesz znaleźć w oficjalnej dokumentacji React dotyczącej hooków.


Jaka jest różnica między useCallback a useMemo w praktyce?

Przy użyciu useCallback memoizujesz funkcje, natomiast useMemo służy do memoizowania dowolnej obliczonej wartości.

const fn = () => 42; // zakładamy kosztowne obliczenia
const memoFn = useCallback(fn, [dep]); // (1)
const memoFnReturn = useMemo(fn, [dep]); // (2)

W przypadku (1) otrzymujemy memoizowaną wersję funkcji fn – ta sama referencja zostanie zachowana przy kolejnych renderowaniach, o ile zależność dep nie ulegnie zmianie. Jednak za każdym razem, gdy wywołujesz memoFn, zaczyna się od nowa kosztowne obliczenie.

Natomiast (2) powoduje, że funkcja fn zostanie wywołana tylko gdy zmieni się dep, a jej zwrócona wartość (tutaj 42) zostanie zapamiętana i przechowywana w zmiennej memoFnReturn.

Dodatkowe informacje na temat obu hooków możesz znaleźć w artykule na blogu o optymalizacji Reacta.


Kiedy warto użyć flushSync w ReactJS?

Wersja React 18 wprowadziła usprawnienia wydajnościowe, dzięki czemu automatycznie grupowane są (batching) wielokrotne aktualizacje stanu podczas pojedynczego renderowania komponentu.

  • Aby wyłączyć automatyczny batching, możesz użyć flushSync, co spowoduje renderowanie komponentu po każdej aktualizacji stanu. Może to być potrzebne, gdy jakiś fragment kodu wymaga natychmiastowego odczytu elementów z DOM po zmianie stanu.

Przykład przy automatycznym batchingu:

function handleClick() {
    setCount(c => c + 1);
    setFlag(f => !f);
    // React wykona tylko jedno renderowanie na końcu (to właśnie batching!)
}

Przykład z użyciem flushSync:

import {flushSync} from 'react-dom';

function handleClick() {
    flushSync(() => {
        setCounter(c => c + 1);
    });
    // DOM został już zaktualizowany
    flushSync(() => {
        setFlag(f => !f);
    });
    // DOM został już zaktualizowany
}

Szczegóły dotyczące flushSync są opisane w dokumentacji Reacta.


Czy niestandardowy hook Reacta może zwracać JSX?

Chociaż nie ma sztywnej reguły, która określałaby, jak definiować niestandardowe hooki i jaka logika powinna się w nich znaleźć, zwracanie JSX z hooka jest antywzorcem. Istnieje kilka negatywnych aspektów takiego rozwiązania:

  • Gdy tworzysz hook, który zwraca komponent JSX, de facto definiujesz ten komponent wewnątrz funkcjonalnego komponentu. W rezultacie przy każdym renderowaniu tworzona jest nowa instancja komponentu. Może to powodować wielokrotne odmontowywanie i ponowne montowanie komponentu, co negatywnie wpływa na wydajność. Dodatkowo, jeśli komponent posiada własny stan, jego stan zostanie zresetowany przy każdym renderowaniu komponentu nadrzędnego.
  • Definiując komponent JSX wewnątrz hooka, pozbawiasz siebie możliwości zastosowania mechanizmu lazy loadingu (leniwego ładowania) dla tego komponentu.
  • Jakakolwiek optymalizacja wydajnościowa komponentu wymaga użycia hooka useMemo, który nie daje takiej elastyczności jak możliwość porównywania właściwości w React.memo().

Zaletą tego rozwiązania mogłaby być możliwość kontrolowania stanu komponentu ze strony komponentu nadrzędnego. Jednak analogiczną logikę można zaimplementować, stosując podejście sterowanego komponentu (controlled component).

Dodatkowe informacje oraz przykłady dotyczące tworzenia niestandardowych hooków znajdziesz w artykule na FullStack.Cafe oraz w książce "The Road to React".


Różnica pomiędzy użyciem useMemo a kombinacją useEffect + useState

Rozważmy dwa podejścia:

Podejście 1 – useEffect + useState

import { expensiveCalculation } from "foo";

function useCalculate(someNumber: number): number {
  const [result, setResult] = useState<number>(null);

  useEffect(() => {
    setResult(expensiveCalculation(someNumber));
  }, [someNumber]);

  return result;
}

W tym rozwiązaniu – przy każdej zmianie zależności (w tym przypadku someNumber) – hook useEffect powoduje wywołanie funkcji, która aktualizuje stan za pomocą setResult. To z kolei skutkuje podwójnym renderowaniem komponentu: pierwsze renderowanie odbywa się z przestarzałymi danymi (lub wartością początkową), a następnie po zaktualizowaniu stanu następuje kolejny render.

Podejście 2 – useMemo

import { expensiveCalculation } from "foo";

function useCalculateWithMemo(someNumber: number): number {
  return useMemo(() => {
    return expensiveCalculation(someNumber);
  }, [someNumber]);
}

Wersja z useMemo oblicza wartość podczas renderowania i zwraca wynik bez konieczności kolejnego renderu. Efektem jest natychmiastowe uzyskanie prawidłowej wartości, co eliminuje opóźnienie wynikające z asynchronicznego ustawiania stanu.

Podsumowanie

  • Wydajność: Chociaż częstotliwość wywołań funkcji expensiveCalculation jest identyczna w obu podejściach, podejście z useEffect + useState skutkuje dodatkowymi renderami – najpierw z wartością początkową (np. null), a następnie z nowo obliczoną wartością. To może negatywnie wpływać na wydajność, szczególnie przy intensywnych obliczeniach.
  • Czytelność: Użycie useMemo sprawia, że kod jest bardziej przejrzysty i zwięzły, eliminując zbędne operacje oraz dodatkowe renderowanie.

Polecane materiały:


Jak zoptymalizować kod z wykorzystaniem React Hooks

Rozważmy poniższy kod, w którym mamy do czynienia z listą użytkowników (wyobraźmy sobie, że jest ich 100000):

import React from 'react';
 
const users = [
  { id: 'a', name: 'Robin' },
  { id: 'b', name: 'Dennis' },
  // ...
];
 
const App = () => {
  const [text, setText] = React.useState('');
  const [search, setSearch] = React.useState('');
 
  const handleText = (event) => {
    setText(event.target.value);
  };
 
  const handleSearch = () => {
    setSearch(text);
  };
 
  const filteredUsers = users.filter((user) => {
    return user.name.toLowerCase().includes(search.toLowerCase());
  });
 
  return (
    <div>
      <input type="text" value={text} onChange={handleText} />
      <button type="button" onClick={handleSearch}>
        Search
      </button>
 
      <List list={filteredUsers} />
    </div>
  );
};
 
const List = ({ list }) => {
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
};
 
const ListItem = ({ item }) => {
  return <li>{item.name}</li>;
};
 
export default App;

W tym przypadku, mimo że przetwarzanie listy użytkowników (filteredUsers) odbywa się tylko po kliknięciu przycisku (zmiana stanu search), funkcja filtra jest wykonywana przy każdym naciśnięciu klawisza, gdy użytkownik wpisuje tekst. Nawet jeżeli lista nie zmienia się dynamicznie podczas wpisywania, wywołanie funkcji filtra dla każdej ostatniej wartości inputu może generować zbędne obciążenie – szczególnie przy bardzo dużej liczbie elementów.

Proponowana optymalizacja

Wykorzystując hooka useMemo, możemy zmemoizować wynik filtrowania, tak aby funkcja filtrowania była wywoływana tylko wtedy, gdy zależność (w tym przypadku search) ulegnie zmianie:

function App() {
  const [text, setText] = React.useState('');
  const [search, setSearch] = React.useState('');
 
  const handleText = (event) => {
    setText(event.target.value);
  };
 
  const handleSearch = () => {
    setSearch(text);
  };
 
  const filteredUsers = React.useMemo(() =>
    users.filter((user) => {
      console.log('Filter function is running ...');
      return user.name.toLowerCase().includes(search.toLowerCase());
    }), [search]
  );
 
  return (
    <div>
      <input type="text" value={text} onChange={handleText} />
      <button type="button" onClick={handleSearch}>
        Search
      </button>
 
      <List list={filteredUsers} />
    </div>
  );
}

Dzięki zastosowaniu useMemo funkcja filtrująca jest wykonywana tylko wtedy, gdy zmienia się stan search – co w rezultacie zmniejsza liczbę obliczeń podczas wpisywania tekstu. Jest to szczególnie ważne, gdy mamy do czynienia z bardzo dużymi zbiorami danych.

Polecane materiały:


Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.