Fetch do tej pory
Zaczniemy może od przyjrzenia się, jak taki fetch wyglądał do tej pory w klasach:
export class App extends React.Component {
state = {
contacts: [],
};
componentDidMount() {
fetch('https://randomuser.me/api/?format=json&results=10')
.then((res) => res.json())
.then((json) => this.setState({ contacts: json.results }));
}
render() {
return <ContactsList contacts={this.state.contacts} />;
}
}
Co tu się dokładnie dzieje? Najpierw deklaruję state, następnie w componentDidMount
wykonuję zapytanie do API, czekam na wynik i rezultat zapisuję do stanu. Ten kod pochodzi ze starszego wpisu, tam też znajdziesz więcej wyjaśnień:
Nie wygląda skomplikowanie, ale czy może być jeszcze prostsze?
Wchodzą Hooki
Z użyciem znanych Ci już Hooków useState
i useEffect
wygląda to tak:
function App() {
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch('https://randomuser.me/api/?format=json&results=10')
.then((res) => res.json())
.then((json) => setContacts(json.results));
}, []);
return <ContactsList contacts={contacts} />;
}
Zwróć uwagę na jedną ważną rzecz: Jako drugi argument do useEffect
podałem tutaj pustą tablicę. Po co? Aby fetch
nie wykonywał się przy każdym renderze, a tylko za pierwszym razem!
No ładnie, prawda? Ale wcale nie jest dużo krótsze. Dodajmy więc nowe wymaganie…
Aktualizacja wyników gdy zmienia się ID
Załóżmy, że Twój komponent ma wyświetlać dane pobrane z API dla danego ID przekazanego mu jako props. W użyciu:
<App id={…} />
Gdy id
się zmieni, komponent ma pobrać dane na nowo i wyświetlić. Jak to wygląda w klasie?
export class App extends React.Component {
state = {
contacts: [],
};
fetchData() {
fetch(`https://randomuser.me/api/?format=json&results=10&seed=${this.props.id}`)
.then((res) => res.json())
.then((json) => this.setState({ contacts: json.results }));
}
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData();
}
}
render() {
return <ContactsList contacts={this.state.contacts} />;
}
}
Łołoło, trochę nam ten komponent urósł 🤔 A z hookami?
function App({ id }) {
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch(`https://randomuser.me/api/?format=json&results=10&seed=${id}`)
.then((res) => res.json())
.then((json) => setContacts(json.results));
}, [id]);
return <ContactsList contacts={contacts} />;
}
To jest nadal tak samo krótkie, jak poprzednio. Zmieniły się tylko 3 fragmenty kodu: Props jako argument do komponentu, fetch pod inny adres i tablica jako drugi argument do useEffect
już nie jest pusta: [id]
.
W tym kodzie jest bug
Jest subtelny i pewnie się na niego nie natkniesz, ale uwierz mi, że w tym kodzie jest bug. Gdy szybko zmienisz wartość ID, wykonają się w krótkim czasie dwa żądania do API. A co się stanie, jeśli odpowiedź na pierwsze żądanie przyjdzie później niż na drugie? Komponent wyświetli nieaktualne dane. Bug!
Aby go naprawić, trzeba anulować poprzednie żądanie. Jak to wygląda w klasie?
export class App extends React.Component {
state = {
contacts: [],
};
controller = new AbortController();
fetchData() {
this.controller.abort();
fetch(`https://randomuser.me/api/?format=json&results=10&seed=${this.props.id}`, { signal: this.controller.signal })
.then((res) => res.json())
.then((json) => this.setState({ contacts: json.results }));
}
componentDidMount() {
this.fetchData();
}
componentWillUnmount() {
this.controller.abort();
}
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.fetchData();
}
}
render() {
return <ContactsList contacts={this.state.contacts} />;
}
}
Używam tutaj eksperymentalnego AbortController
, więc przestudiuj ten kod dokładnie :) Aby anulować żądanie, wywołuję controller.abort()
— w klasie musiałem sobie zapisać instancję AbortController
.
Dodatkowo dopisałem też brakujące componentWillUnmount
, aby komponent nie pobierał niepotrzebnie danych, gdy już zostanie odmontowany.
Z Hookami jest dużo prościej, bo korzystasz z naturalnego dla JS domknięcia:
function App({ id }) {
const [contacts, setContacts] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`https://randomuser.me/api/?format=json&results=10&seed=${id}`, { signal: controller.signal })
.then((res) => res.json())
.then((json) => setContacts(json.results));
return () => controller.abort();
}, [id]);
return <ContactsList contacts={contacts} />;
}
Tak, to już!
Hooki są czystsze
Zwróć uwagę jak bardo porozrzucany jest kod w ostatnim przykładzie w klasie — constructor
, componentDidMount
, componentWillUnmount
i componentDidUpdate
. W hookach zaś, wszystko jest obok siebie. Powiązany kod leży blisko.
Dodatkowo, łatwo mogłabyś się pokusić o stworzenie nowego hooka, który by całą tę logikę enkapsulował. Dzięki temu mogłabyś łatwo go używać w różnych miejscach, bez konieczności duplikowania kodu. To w praktyce niemożliwe przy użyciu klas (chyba, że przez HoC lub zagnieżdżając kolejne komponenty…)
Pytania?
zapisz się na szkolenie z React. Jeśli chcesz na bieżąco śledzić kolejne części kursu React.js to koniecznie polub mnie na Facebooku i zapisz się na newsletter.