Перейти до основного вмісту

HTTP-запити

Припустимо, що вже є дерево компонентів, яке має кілька рівнів ієрархії, і необхідно отримати колекцію елементів від API. Який саме компонент в ієрархії повинен відповідати за HTTP-запити і зберігання результату відповіді? Якщо не використовуємо бібліотеку управління станом, то це залежить від трьох критеріїв.

  • Яким компонентам будуть необхідні отримані дані?
  • Де буде рендеритися індикатор завантаження, доки виконується HTTP-запит?
  • Де буде рендеритися повідомлення у разі помилки HTTP-запиту?

Методи життєвого циклу componentDidMount та componentDidUpdate ідеально підходять для HTTP-запитів. Коли викликається componentDidMount(), компонент вже був відрендерений у DOM і готовий до подальшого оновлення стану. Коли викликається componentDidUpdate(), пропи або стан компонента змінилися, і, можливо, необхідно зробити новий запит, попередньо порівнявши їх, щоб не зациклити рендер компонента.

Для HTTP-запиту можна використовувати будь-що: XMLHTTPRequest, fetch, axios, superagent тощо. Ми будемо використовувати бібліотеку axios.

npm install axios

Запити будемо робити на Hacker News API. Після завершення HTTP-запиту зберігаємо результат у стані компонента. У методі render використовуємо стан.

import React, { Component } from "react";
import axios from "axios";

axios.defaults.baseURL = "https://hn.algolia.com/api/v1";

const ArticleList = ({ articles }) => (
<ul>
{articles.map(({ objectID, url, title }) => (
<li key={objectID}>
<a href={url} target="_blank" rel="noreferrer noopener">
{title}
</a>
</li>
))}
</ul>
);

class App extends Component {
state = {
articles: [],
};

async componentDidMount() {
const response = await axios.get("/search?query=react");
this.setState({ articles: response.data.hits });
}

render() {
const { articles } = this.state;
return (
<div>
articles.length > 0 ? <ArticleList articles={articles} /> : null
</div>
);
}
}

Крім поля зберігання даних, можна додати поле для зберігання прапорця індикатора завантаження і помилки. Це дозволить зробити використання інтерфейсу приємнішим для користувача.

Індикатор завантаження

Доки чекаємо на відповідь на HTTP-запит, показуємо індикатор завантаження. Щойно надійшла відповідь, ховаємо індикатор. Для цього, на старті запиту ставимо isLoadingtrue, а у разі успішної відповіді або помилки – false.

/* ... */

class App extends Component {
state = {
articles: [],
isLoading: false,
};

async componentDidMount() {
this.setState({ isLoading: true });
const response = await axios.get("/search?query=react");
this.setState({
articles: response.data.hits,
isLoading: false,
});
}

/* ... */
}

В методі render за умовою повертаємо розмітку. Якщо дані завантажуються, показуємо лоадер, в іншому випадку – список з результатами.

/* ... */

class App extends Component {
/* ... */

render() {
const { articles, isLoading } = this.state;
return (
<div>
isLoading ? <p>Loading...</p> : <ArticleList articles={articles} />
</div>
);
}
}

Індикатор завантаження може бути будь-чим: від простого тексту або спінера до кастомного компонента, наприклад react-content-loader.

Обробка помилки

HTTP-запит не завжди виконується без помилок, тому користувачеві обов'язково потрібно дати зрозуміти, якщо щось пішло не так. Для цього у стан додаємо властивість зберігання помилки.

При використанні промісів для обробки помилок використовується блок catch, якщо він виконається, значить, сталася помилка. Встановлення індикатора завантаження переносимо в блок finally, щоб не дублювати код, який буде виконаний в будь-якому випадку.

/* ... */

class App extends Component {
state = {
articles: [],
isLoading: false,
error: null,
};

async componentDidMount() {
this.setState({ isLoading: true });

try {
const response = await axios.get("/search?query=react");
this.setState({ articles: response.data.hits });
} catch (error) {
this.setState({ error });
} finally {
this.setState({ isLoading: false });
}
}

/* ... */
}

Залишилось доповнити метод render.

/* ... */
class App extends Component {
/* ... */
render() {
const { articles, isLoading, error } = this.state;

return (
<div>
{error && <p>Whoops, something went wrong: {error.message}</p>}
{isLoading && <p>Loading...</p>}
{articles.length > 0 && <ArticleList articles={articles} />}
</div>
);
}
}

Поділ відповідальності

Зберігати код, пов'язаний з HTTP-запитом, безпосередньо в компоненті – не найкраща практика. У застосунку буде багато різних запитів до API і вони будуть використовуватися у різних компонентах. До того ж код HTTP-запитів може бути складним та громіздким. Для зручності рефакторингу будемо все зберігати в одному місці.

Створимо додаткову папку всередині src. Назва папки довільна, але логічна, наприклад helpers, api, services тощо. У цій папці будемо зберігати файл з функціями для HTTP-запитів.

// services/api.js
import axios from "axios";

export const fetchArticlesWithQuery = async searchQuery => {
const response = axios.get(`/search?query=${searchQuery}`);
return response.data.hits;
};

export default {
fetchArticlesWithQuery,
};

Імпортуємо сервіс у файлі компонента та викликаємо потрібний метод.

/* ... */
import api from "./path/to/services/api";

class App extends Component {
state = {
articles: [],
isLoading: false,
error: null,
};

async componentDidMount() {
this.setState({ isLoading: true });

try {
const articles = api.fetchArticlesWithQuery("react");
this.setState({ articles });
} catch (error) {
this.setState({ error });
} finally {
this.setState({ isLoading: false });
}
}

/* ... */
}