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>)}
</>
)
}
zarządza lokalną tablicą stanu, czyli
useStatestate
. 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 ]
}
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.
useFetchuseFetch
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.