Omawiamy sobie znaleziony w internecie na Githubie świetny hook useFetch, świetny przykład programowania asynchronicznego w React. Do dzieła.
Ok, najpierw zobaczmy na plik http.js, typowy plik utils z funkcjami:
export async function fetchAvailablePlaces() {
const response = await fetch('http://localhost:3000/places');
const resData = await response.json();
if (!response.ok) {
throw new Error('Failed to fetch places');
}
return resData.places;
}
Co tu mamy:
- Funkcja ze słowem kluczowym export, czyli można ją importować (robimy to po to, aby nie robić syfu w plikach z RFC lub jeszcze gorzej – umieścić kod wewnątrz RFC, co jak wiemy powoduje definiowane funkcji przy każdym rerenderze)
- Słówko kluczowe async, czyli funkcja może używać await – wtedy czeka, aż wykona się promise, na który czeka, a JS może bez blokowania dalej wykonywać swój kod – to, co po await wykona się dopiero, gdy await się skończy, taki odpowiednik then z promise, tylko z jedną funkcją ogarniającą zarówno onFulfilled i onRejected
- Mamy dwa awaity… Dlaczego? Cóż, fetch zwraca promise i response.json zwraca promise… My „od razu” (od fetch) dostajemy tylko bardzo ograniczoną odpowiedź ze statusem http i takie tam, na body musimy czekać (bo sama operacja zamiany body w formacie json na obiekt JS to byłaby synchroniczna funkcja JSON parse)
- Ok, sprawdzamy, czy response.ok po awaitach… albo między jednym a drugim, to nie ma znaczenia tak naprawdę (sprawdziłem, wcześniej mi się wydawało, że powinno to być między jednym a drugim, nie ma znaczenia)
- Jako że zwracamy error w przypadku błędów to będzie to można „normalnie” obsłużyć, bo ta składnia promises może być dla niektórych nieco myląca (powiemy sobie i o tym niedługo)
Ok, teraz miejsce, RFC, gdzie nasz hook (jeszcze go zobaczymy) będzie używany:
import Places from './Places.jsx';
import Error from './Error.jsx';
import { sortPlacesByDistance } from '../loc.js';
import { fetchAvailablePlaces } from '../http.js';
import { useFetch } from '../hooks/useFetch.js';
async function fetchSortedPlaces() {
const places = await fetchAvailablePlaces();
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
places,
position.coords.latitude,
position.coords.longitude
);
resolve(sortedPlaces);
});
});
}
Jak widać funkcja powyżej poziomu RFC, aby nie była definiowana od nowa przy każdym rerenderze RFC. Czeka na wykonanie tamtej funkcji, zwraca promise, która używa geolokacji i funkcji sortującej (z jakiejś biblioteczki) do sortowania tych miejsc po naszej lokacji.
Czyli tak:
- await zwraca promise, dopóki się nie wykona, flow programu jest zwolnione
- to, co po await to taki jakby then z promises, ale z onFulfilled i onRejected w jednym
- tu mamy zwrócenie nowej promisy, która tylko resolve przyjmuje
- geolokację też już omawialiśmy
- gdy cały kod się wykona to resolve sprawia, że będzie można do tej promise podpiąć then i wykonać jakiś callback (albo zrobić await na fetchSortedPlaces)
Ok, zobaczmy użycie w RFC:
export default function AvailablePlaces({ onSelectPlace }) {
const {
isFetching,
error,
fetchedData: availablePlaces,
} = useFetch(fetchSortedPlaces, []);
if (error) {
return <Error title="An error occurred!" message={error.message} />;
}
return (
<Places
title="Available Places"
places={availablePlaces}
isLoading={isFetching}
loadingText="Fetching place data..."
fallbackText="No places available."
onSelectPlace={onSelectPlace}
/>
);
}
Ok, jakiś custom hook. Od razu chciałbym uwrażliwić – ta pusta tablica będzie używana jako initial value. Dlatego można tak ją sobie przekazać. Czasem przekazywanie pustych tablic/obiektów (zamiast np. nazwy zmiennej wskazującej na pusty obiekt powyżej poziomu RFC) powoduje, że jakieś zależności się psują, bo ten pusty obiekt jest odtwarzany pod nową referencją i jest pętla nieskończona.
Jak widać zwraca isFetching, error oraz fetchedData. Nie mamy żadnych useEffectów, setState’ów ani innych rzeczy, więc możemy się domyślić, że tym zajmuje się nasz hook, nam pozostawiając deklaratywne określanie czego chcemy.
I tak jest, zobaczmy custom hook useFetch:
import { useEffect, useState } from 'react';
export function useFetch(fetchFn, initialValue) {
const [isFetching, setIsFetching] = useState();
const [error, setError] = useState();
const [fetchedData, setFetchedData] = useState(initialValue);
useEffect(() => {
async function fetchData() {
setIsFetching(true);
try {
const data = await fetchFn();
setFetchedData(data);
} catch (error) {
setError({ message: error.message || 'Failed to fetch data.' });
}
setIsFetching(false);
}
fetchData();
}, [fetchFn]);
return {
isFetching,
fetchedData,
setFetchedData,
error
}
}
Fetchfn się nie zmieni, bo jest powyżej RFC. Callback useEffect nie może być async, ale wewnątrz niego można użyć async. Cała logika jest bardzo prosta moim zdaniem, natomiast gdyby nas ta abstrakcja dowaliła, to mamy na tym samym githubie tego samego autora taki kod, też async też useEffect (już to omawialiśmy) ale wewnątrz RFC:
import { useState, useEffect } from 'react';
import MealItem from './MealItem.jsx';
export default function Meals() {
const [loadedMeals, setLoadedMeals] = useState([]);
useEffect(() => {
async function fetchMeals() {
const response = await fetch('http://localhost:3000/meals');
if (!response.ok) {
// ...
}
const meals = await response.json();
setLoadedMeals(meals);
}
fetchMeals();
}, []);
return (
<ul id="meals">
{loadedMeals.map((meal) => (
<MealItem key={meal.id} meal={meal} />
))}
</ul>
);
}
No a my zrobiliśmy tak, że można sobie zdefiniować funkcję gdzie indziej, w RFC użyć hooka i nie mieć tych useEffectów i setStejtów bez potrzeby (nawet isFetching mamy, którego możemy użyć), pełna deklaratywność, zero głupot, zero długiego, niezbyt modularnego i przesadnie imperatywnego pisania kodu.