Chidume Nnamdi
Chidume NnamdiTech Writer @ Log Rocket

Pobieranie danych w React za pomocą hooków

Sprawdź, jak pobierać dane w React, korzystając przy tym z komponentów funkcyjnych zamiast klasowych i jak może w tym pomóc React Hooks.
1.10.20217 min
Pobieranie danych w React za pomocą hooków

Komponenty klasowe mogą być uciążliwe. W wielu przypadkach jesteśmy zmuszeni powielać logikę w różnych metodach cyklu życia, aby zaimplementować „logikę efektów”. Nie oferują one też eleganckiego rozwiązania współdzielenia jej między komponentami (HOC i reszta nie są eleganckim rozwiązaniem). Z drugiej strony React Hooks dają nam możliwość budowania niestandardowych hooków, co jest tutaj o wiele lepszym i prostszym rozwiązaniem.

Krótko mówiąc, komponenty funkcyjne z hookami są znacznie bardziej w duchu Reacta. Udostępnianie i ponowne używanie komponentów jest dzięki nim znacznie łatwiejsze.

Pobieranie danych przy pomocy komponentów klasowych

Podczas pracy ze zwykłymi komponentami klasowymi w React, aby bez problemu pobierać dane z serwera i je wyświetlać, korzystamy z metod cyklu życia.

Spójrzmy na prosty przykład:

class App extends Component {

     this.state = {
          data: []
     }
    componentDidMount() {
        fetch("/api/data").then(
            res => this.setState({...this.state, data: res.data})
        )
    }
    render() {
        return (
            <>
                {this.state.data.map( d => <div>{d}</div>)}
            </>
        )
    }
}


Po zamontowaniu komponent pobierze dane i wyrenderuje je. Zauważ, że nie umieściliśmy logiki pobierania w konstruktorze. Zamiast tego przekazaliśmy ją do hooka componentDidMount. Żądania sieciowe mogą zająć trochę czasu — lepiej nie wstrzymywać się z montowaniem komponentu.

Rozwiązujemy obietnicę zwróconą przez wywołanie fetch(...) i ustawiamy stan data na dane z odpowiedzi. To z kolei spowoduje ponowne wyrenderowanie komponentu (aby wyświetlić nowe dane w stanie komponentu).

Od komponentu klasowego do komponentu funkcyjnego

Powiedzmy, że chcemy zmienić komponent klasowy na komponent funkcyjny. Jak to zaimplementować, aby poprzednie zachowanie pozostało takie samo?


useState i useEffect

useState jest hookiem używanym do utrzymywania lokalnych stanów w komponentach funkcyjnych. useEffect służy z kolei do wykonywania funkcji po wyrenderowaniu komponentu (w celu „wykonania efektów ubocznych”). useEffect można ograniczyć do przypadków, w których zmienia się wybrany zestaw wartości. Wartości te nazywane są „zależnościami”. useEffect wykonuje łącznie zadania componentDidMount, componentDidUpdate i componentWillUpdate.

Hooki te zapewniają nam wszystkie narzędzia, które otrzymaliśmy wcześniej z podejścia klasowego i metod cyklu życia. Zróbmy więc refaktoryzację aplikacji z komponentu klasowego do komponentu funkcyjnego.

function App() {
    const [state, setState] = useState([])
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        )
    })
    return (
        <>
            {state.map( d => <div>{d}</div>)}        
        </>
    )
}


useState
zarządza lokalną tablicą stanu, czyli state. useEffect wyśle żądanie sieciowe podczas renderowania komponentu. Kiedy pobieranie się zakończy, żądanie zmieni stan lokalny odpowiedzią z serwera za pomocą funkcji setState. To z kolei spowoduje renderowanie komponentu w celu zaktualizowania modelu DOM o dane.


Zapobieganie niekończącym się wywołaniom zwrotnym za pomocą zależności

Mamy problem. useEffect działa, gdy komponent jest montowany i aktualizowany. W powyższym kodzie useEffect uruchomi się, gdy aplikacja zostanie zamontowana i podczas gdy wywoływana jest funkcja setState (po rozwiązaniu pobierania). To jednak nie wszystko — metoda useEffect zostanie ponownie uruchomiona w wyniku renderowania komponentu.

Jak się zapewne domyśliliście, da to niekończący się cykl wywołań zwrotnych.

Jak wspomniano wcześniej, useEffect ma drugi parametr, czyli „zależności”. Określają one, w jakich przypadkach useEffect powinien reagować na aktualizowany komponent. Zależności ustawiane są jako tablice. Tablica będzie zawierała zmienne, dla których zostanie sprawdzone czy zmieniły się one od ostatniego renderowania. Jeśli któraś z nich się zmieni, useEffect zostanie wykonane, a jeśli nie, to będziemy mieli sytuację odwrotną.

useEffect(()=> {
    ...
}, [dep1, dep2])


Pusta tablica zależności zapewnia, że useEffect zostanie uruchomiony tylko raz, gdy komponent jest montowany.

function App() {
    const [state, setState] = useState([])
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        )
    }, [])
    return (
        <>
            {state.map( d => <div>{d}</div>)}      
        </>
    )
}


Implementacja komponentu funkcyjnego jest teraz taka sama, jak nasza początkowa implementacja z użyciem komponentu klasowego. Oba zadziałają w momencie montowania komponentu, by pobrać dane. Przy następnych aktualizacjach nie będą już jednak nic robić.


Memoizacja przy użyciu zależności

Spójrzmy na przypadek, w którym możemy użyć zależności do memoizacji useEffect.

Powiedzmy, że mamy komponent, który pobiera dane z zapytania.

function App() {
    const [state, setState] = useState([])
    const [query, setQuery] = useState()
    useEffect(() => {
        fetch("/api/data?q=" + query).then(
            res => setState(res.data)
        )
    }, [query])
    function searchQuery(evt) {
        const value = evt.target.value
        setQuery(value)
    }
    return (
        <>
            {state.map( d => <div>{d}</div>)}<input type="text" placeholder="Type your query" onEnter={searchQuery} />  
        </>
    )
}


Mamy stan zapytania, w którym przechowywany jest parametr wyszukiwania. Zostanie on wysłany do API.

Zapamiętaliśmy useEffect, przekazując stan zapytania do tablicy zależności. Spowoduje to, że useEffect załaduje dane dla zapytania przy aktualizacji/ponownym renderowaniu, tylko wtedy, gdy zapytanie ulegnie zmianie. Bez zapamiętywania useEffect stale będzie ładował dane z punktu końcowego, nawet jeśli zapytanie nie uległo zmianie, co powoduje niepotrzebne ponowne renderowanie w komponencie.

Mamy więc podstawową implementację tego, jak można pobierać dane w komponentach funkcyjnych Reacta za pomocą hooków useState i useEffect. useState służy do utrzymywania odpowiedzi danych z serwera w komponencie.

Hook useEffect jest tym, czego używaliśmy do pobierania danych z serwera (ponieważ jest to efekt uboczny). Daje nam on podobne możliwości, jak metody cyklu życia dostępne tylko dla komponentów klasowych, dzięki czemu możemy pobierać lub aktualizować dane w momencie montowania i aktualizacji.

Obsługa błędów

Nic nie przychodzi bez błędów. Skonfigurowaliśmy pobieranie danych za pomocą hooków, ale co się stanie, jeśli żądanie pobrania zakończy się błędem? Jak zareaguje komponent aplikacji?

Trzeba obsłużyć błędy, które mogą się zdarzyć przy pobieraniu danych.


Obsługa błędów w komponencie klasowym

Zobaczmy, jak możemy to zrobić w komponencie klasowym:

class App extends Component {
    constructor() {
        this.state = {
            data: [],
            hasError: false
        }
    }
    componentDidMount() {
        fetch("/api/data").then(
            res => this.setState({...this.state, data: res.data})
        ).catch(err => {
            this.setState({ hasError: true })
        })
    }
    render() {
        return (
            <>
                {this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))}
            </>
        )
    }
}


Dodaliśmy teraz hasError do stanu lokalnego z domyślną wartością false (tak, powinno być false, ponieważ podczas inicjalizacji komponentu nie nastąpiło jeszcze pobieranie danych). W metodzie render użyliśmy operatora trójargumentowego do sprawdzenia flagi hasError w stanie komponentu.

Ponadto dodaliśmy obietnicę catch do wywołania fetch, aby ustawić stan hasError na true, gdy pobieranie danych nie powiedzie się.


Obsługa błędów w komponencie funkcyjnym

function App() {
    const [state, setState] = useState([])
    const [hasError, setHasError] = useState(false)
    useEffect(() => {
        fetch("/api/data").then(
            res => setState(res.data)
        ).catch(err => setHasError(true))
    }, [])
    return (
        <>
            {hasError? <div>Error occured.</div> : (state.map( d => <div>{d}</div>))}      
        </>
    )
}

Dodanie wskaźnika „Loading...”


Ładowanie w komponencie klasowym

Zobaczmy implementację w komponencie klasowym:

class App extends Component {
    constructor() {
        this.state = {
            data: [],
            hasError: false,
            loading: false
        }
    }
    componentDidMount() {
        this.setState({loading: true})
        fetch("/api/data").then(
            res => {
                this.setLoading({ loading: false})
                this.setState({...this.state, data: res.data})
                }
        ).catch(err => {
            this.setState({loading: false})
            this.setState({ hasError: true })
        })
    }
    render() {
        return (
            <>
                {
                    this.state.loading ? <div>loading...</div> : this.state.hasError ? <div>Error occured fetching data</div> : (this.state.data.map( d => <div>{d}</div>))}
            </>
        )
    }
}


Deklarujemy stan utrzymujący flagę ładowania. Następnie w componentDidMount flaga ładowania jest ustawiana na wartość true, co powoduje ponowne renderowanie komponentu w celu wyświetlenia komunikatu „loading ...”.


Ładowanie w komponencie funkcyjnym 

Zobaczmy implementację funkcyjną:

   useEffect(() => {
        setLoading(true)
        fetch("/api/data").then(
            res => {
                setState(res.data);
                setLoading(false)}
        ).catch(err => {
            setHasError(true))
            setLoading(false)})
    }, [])
    return (
        <>
            {
                loading ? <div>Loading...</div> : hasError ? <div>Error occured.</div> : (state.map( d => <div>{d}</div>))
            }
        </>
    )
}


Będzie to działać w taki sam sposób, jak poprzedni komponent klasowy.

Dodaliśmy kolejny stan przy użyciu useState. Będzie on utrzymywał flagę ładowania. Początkowo jest ona ustawiona na false, więc gdy aplikacja zostanie zamontowana, useEffect ustawi ją na true (i pojawi się komunikat „loading…”).

Następnie, po pobraniu danych lub wystąpieniu błędu, stan ładowania jest ustawiany na false, więc komunikat „Loading…” znika i jest zastępowany przez wynik zwrócony przez obietnicę.

Opakowanie wszystkiego w moduł Node

Opakujmy wszystko, co zrobiliśmy, w moduł Node. Stworzymy niestandardowy hook, który będzie używany do pobierania danych z punktu końcowego w komponentach funkcyjnych.

function useFetch(url, opts) {
    const [response, setResponse] = useState(null)
    const [loading, setLoading] = useState(false)
    const [hasError, setHasError] = useState(false)
    useEffect(() => {
        setLoading(true)
        fetch(url, opts)
            .then((res) => {
            setResponse(res.data)
            setLoading(false)
        })
            .catch(() => {
                setHasError(true)
                setLoading(false)
            })
    }, [ url ])
    return [ response, loading, hasError ]
}


useFetch
to niestandardowy hook, który można wykorzystać w komponentach funkcyjnych do pobierania danych. Połączyliśmy każdy temat, który omawialiśmy, w jeden niestandardowy hook. useFetch zapamiętuje adres URL, z którego pobrane zostaną dane, przekazując parametr adresu URL do tablicy zależności. useEffect będzie zawsze działać po przekazaniu nowego adresu URL.

Możemy użyć niestandardowego hooka w naszych komponentach funkcyjnych.

function App() {
    const [response, loading, hasError] = useFetch("api/data")
    return (
        <>
            {loading ? <div>Loading...</div> : (hasError ? <div>Error occured.</div> : (response.map(data => <div>{data}</div>)))}
        </>
    )
}


Proste.

Podsumowanie

Sprawdziliśmy, jak używać hooków useState i useEffect do pobierania i utrzymywania danych z punktu końcowego API w komponentach funkcyjnych.


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>