Цель этой заметки – показать вам способ выполнения HTTP GET-запросов с помощью React и пользовательского хука.
🚨 Примечание: Этот пост требует от вас знания основ React (основные хуки и fetch-запросы).
Любой вид обратной связи приветствуется, спасибо и я надеюсь, что вам понравится статья.🤗
- Оглавление
- 🚨 Используемые технологии.
- 〽️ Создание проекта.
- 〽️ Первые шаги.
- 〽️ Делаем наш первый фетч.
- 〽️ Отображение данных API на экране.
- 〽️ Создание пользовательского крючка.
- 〽️ Улучшение хука useFetch.
- 〽️ Добавление новых компонентов и рефакторинг.
- 🟡 Header.tsx
- 🟡 Loading.tsx
- 🟡 ErrorMessage.tsx
- 🟡 Card.tsx
- 🟡 LayoutCards.tsx
- Franklin361 / fetching-data-custom-hook
- Руководство по получению данных и созданию пользовательского хука
- Получение данных и создание пользовательских Hook
Оглавление
📌 Технологии для использования
📌 Создание проекта
📌 Первые шаги
📌 Делаем наш первый забор
📌 Отображение данных API на экране
📌 Создание пользовательского крючка
📌 Усовершенствование крючка
useFetch
.📌 Добавление новых компонентов и рефакторинг
📍 Header.tsx
📍 Loading.tsx
📍 ErrorMessage.tsx
📍 Card.tsx
📍 LayoutCards.tsx
🚨 Используемые технологии.
▶️ React JS (версия 18)
▶️ Vite JS
▶️ TypeScript
▶️ Rick and Morty API
▶️ Vanilla CSS (стили можно найти в репозитории в конце этого поста)
〽️ Создание проекта.
npm init vite@latest
В данном случае мы назовем его: fetching-data-custom-hook
(необязательно).
Выберите React, а затем TypeScript.
Затем выполните следующую команду для перехода в только что созданный каталог.
cd fetching-data-custom-hook
Далее мы устанавливаем зависимости:
npm install
Затем открываем проект в редакторе кода (в моем случае VS code)
code .
〽️ Первые шаги.
Внутри папки src/App.tsx мы удалим все содержимое файла и разместим функциональный компонент, который показывает заголовок и подзаголовок.
const App = () => {
return (
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
)
}
export default App;
Прежде всего, мы создадим пару интерфейсов, которые помогут нам автоматически заполнять свойства, поступающие в JSON-ответе, предоставляемом API.
- Первый интерфейс
Response
содержит свойство results, которое представляет собой массив Results. - Второй интерфейс
Result
, содержит только 3 свойства (хотя их больше, вы можете проверить документацию API), выбираем ID, имя и изображение персонажа.
interface Response {
results: Result[]
}
interface Result {
id: number;
name: string;
image: string;
}
〽️ Делаем наш первый фетч.
- Сначала мы добавляем состояние, которое имеет тип
Result[]
, а значением по умолчанию будет пустой массив, поскольку мы еще не выполнили вызов API. Он будет использоваться для хранения данных API для отображения.
const App = () => {
const [data, setData] = useState<Result[]>([]);
return (
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
)
}
export default App;
- Чтобы выполнить выборку данных, нам нужно сделать это в
useEffect
, так как нам нужно выполнить выборку при первом отображении нашего компонента.
Поскольку нам нужно, чтобы он выполнялся только один раз, мы помещаем пустой массив (т.е. без каких-либо зависимостей).
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
},[]) // arreglo vació
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
</div>
)
}
export default App;
- Внутри тела функции
useEffect
будет сделан вызов API, а посколькуuseEffect
не позволяет нам использовать асинхронный код напрямую, мы сделаем вызов, используя обещания.
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {})
.catch(console.log)
},[])
- Как только обещания будут разрешены, мы получим данные, соответствующие API, которые установим в состояние с помощью функции
setData
.
Теперь мы можем вывести данные на экран. 😌
🚨 Если что-то пойдет не так с API, catch поймает ошибку и покажет ее в консоли, а значение статуса “data
” останется пустым массивом (и в конце не будет показано ничего, кроме названия и подзаголовка приложения).
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
〽️ Отображение данных API на экране.
Перед отображением данных API нам необходимо выполнить оценку. 🤔
🔵 Только если длина значения статуса “data
” больше 0, мы выводим данные API на экран.
🔵 Если длина значения статуса “data
” меньше или равна 0, на экране не будет отображаться никаких данных, только название и субтитр.
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && <p>data</p>
}
</div>
)
}
export default App;
Теперь, когда мы подтвердили, что у нас действительно есть данные в значении состояния “data
“, мы перейдем к отображению и формированию данных.
Использование функции map, применяемой в массивах. Мы пройдем по массиву значения состояния “data
” и вернем новый JSX-компонент, который в данном случае будет представлять собой только изображение и текст.
ПРИМЕЧАНИЕ: свойство key внутри div – это идентификатор, который React использует в списках для более эффективного отображения компонентов. Важно установить его.
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
Таким образом, мы завершили получение данных и корректное отображение их на экране. Но мы все еще можем его улучшить. 😎
〽️ Создание пользовательского крючка.
В папке src/hook мы создаем файл под названием useFetch
.
Создайте функцию и вырежьте логику из компонента App.tsx
.
const App = () => {
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
Вставьте логику в эту функцию, а в конце верните значение состояния “data
“.
export const useFetch = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[]);
return {
data
}
}
Наконец, мы вызываем хук useFetch
, извлекающий данные.
Вот и все, наш компонент стал еще чище и легче для чтения. 🤓
const App = () => {
const { data } = useFetch();
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
Но подождите, мы еще можем улучшить этот крючок. 🤯
〽️ Улучшение хука useFetch
.
Теперь мы улучшим крючок, добавив больше свойств.
К существующему состоянию мы добавим другие свойства, и это новое состояние будет иметь тип DataState
.
interface DataState {
loading: boolean;
data: Result[];
error: string | null;
}
loading, булево значение, позволит нам узнать, когда выполняется вызов API. По умолчанию значение будет установлено в true.
error, строка или нулевое значение, то будет показано сообщение об ошибке. По умолчанию значение будет равно null.
🔵 data, значение типа Result[]
, покажет данные API. По умолчанию значением будет пустой массив.
🔴 ПРИМЕЧАНИЕ: только что переименовал свойства поместья.
🔵 данные ➡️ dataState
🔵 setData ➡️ setDataState
export const useFetch = () => {
const [dataState, setDataState] = useState<DataState>({
data: [],
loading: true,
error: null
});
useEffect(()=> {
fetch('https://rickandmortyapi.com/api/character/?page=8')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[]);
return {
data
}
}
Теперь мы вынесем логику useEffect
в отдельную функцию. Эта функция будет называться handleFetch
.
Мы будем использовать useCallback
, чтобы сохранить эту функцию и предотвратить ее повторное создание при изменении состояния.
useCallback
также получает массив зависимостей, в данном случае мы оставим его пустым, так как хотим, чтобы он был сгенерирован только один раз.
const handleFetch = useCallback(
() => {},
[],
)
Функция, полученная в useCallback
, может быть асинхронной, поэтому мы можем использовать async/await.
- Сначала мы разместим try/catch для обработки ошибок.
- Затем мы создаем константу со значением URL для вызова API.
- Мы выполняем вызов API с помощью fetch и отправляем ему URL (функция await позволит нам дождаться ответа, правильного или неправильного, в случае ошибки он сразу перейдет в функцию catch).
const handleFetch = useCallback(
async () => {
try {
const url = 'https://rickandmortyapi.com/api/character/?page=18';
const response = await fetch(url);
} catch (error) {}
},
[],
)
- Затем мы оцениваем ответ, если есть ошибка, то активируем catch и отправляем ошибку, которую выдает нам API.
- В catch мы собираемся установить состояние. Мы вызываем setDataState, передаем ей функцию для получения предыдущих значений (prev). Мы возвращаем следующее.
- Мы разбрасываем предыдущие свойства (…prev), которые в данном случае будут только значением свойства data, которое в итоге окажется пустым массивом.
- загрузки, мы устанавливаем значение false.
- error, мы устанавливаем значение параметра error, который получает catch, чтобы получить сообщение и поместить его в это свойство.
const handleFetch = useCallback(
async () => {
try {
const url = 'https://rickandmortyapi.com/api/character/?page=18';
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
- Если нет ошибки от API, мы получаем информацию и устанавливаем состояние аналогично тому, как мы это делали в catch.
- Мы вызываем setDataState, передаем ей функцию для получения предыдущих значений (prev). Мы возвращаем следующее.
- Мы распространяем предыдущие свойства (…prev), которые в данном случае будут только значением свойства error, которое в итоге будет равно null.
- загрузки, мы устанавливаем значение false.
- данных, будет значение счетчика dataApi путем обращения к его свойству results.
const handleFetch = useCallback(
async () => {
try {
const url = 'https://rickandmortyapi.com/api/character/?page=18';
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
const dataApi: Response = await response.json();
setDataState( prev => ({
...prev,
loading: false,
data: dataApi.results
}));
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
После создания функции handleFetch
мы возвращаемся к функции useEffect
, удаляем логику и добавляем следующее.
Мы оцениваем, если значение состояния “dataState”, обращаясь к свойству data, содержит длину, равную 0, то мы хотим, чтобы функция была выполнена. Это делается для того, чтобы избежать вызова функции более одного раза.
useEffect(() => {
if (dataState.data.length === 0) handleFetch();
}, []);
И крючок будет выглядеть следующим образом:
🔴 ПРИМЕЧАНИЕ: в конце хука мы возвращаем, используя оператор spread, значение состояния “dataState”.
ПРИМЕЧАНИЕ: интерфейсы были перемещены в соответствующую папку внутри src/interfaces.
import { useState, useEffect, useCallback } from 'react';
import { DataState, Response } from '../interface';
const url = 'https://rickandmortyapi.com/api/character/?page=18';
export const useFetch = () => {
const [dataState, setDataState] = useState<DataState>({
data: [],
loading: true,
error: null
});
const handleFetch = useCallback(
async () => {
try {
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
const dataApi: Response = await response.json();
setDataState( prev => ({
...prev,
loading: false,
data: dataApi.results
}));
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
useEffect(() => {
if (dataState.data.length === 0) handleFetch();
}, []);
return {
...dataState
}
}
Прежде чем использовать новые свойства этого хука, мы проведем рефакторинг и создадим больше компонентов. 😳
〽️ Добавление новых компонентов и рефакторинг.
Первое, что необходимо сделать, это создать папку components внутри src.
Внутри папки components мы создаем следующие файлы.
🟡 Header.tsx
Внутри этого компонента будет только название и субтитр, созданные ранее. 😉.
export const Header = () => {
return (
<>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
</>
)
}
🟡 Loading.tsx
Этот компонент будет показан только в том случае, если свойство load хука установлено в true. ⏳
export const Loading = () => {
return (
<p className='loading'>Loading...</p>
)
}
🟡 ErrorMessage.tsx
Этот компонент будет отображаться только в том случае, если свойство error хука содержит строковое значение. 🚨
export const ErrorMessage = ({msg}:{msg:string}) => {
return (
<div className="error-msg">{msg.toUpperCase()}</div>
)
}
🟡 Card.tsx
Отображает данные API, т.е. изображение и его текст. 🖼️
import { Result } from '../interface';
export const Card = ({ image, name }:Result) => {
return (
<div className='card'>
<img src={image} alt={image} width={100} />
<p>{name}</p>
</div>
)
}
🟡 LayoutCards.tsx
Этот компонент служит в качестве контейнера для компоновки свойства данных и отображения карточек с информацией о них. 🔳
ПРИМЕЧАНИЕ: мы используем memo, заключая наш компонент, чтобы избежать повторного рендеринга, что, вероятно, не будет замечено в данном приложении, но это просто совет. Эта мемо-функция перерисовывается только в том случае, если свойство “data” меняет свои значения.
import { memo } from "react"
import { Result } from "../interface"
import { Card } from "./"
interface Props { data: Result[] }
export const LayoutCards = memo(({data}:Props) => {
return (
<div className="container-cards">
{
(data.length > 0) && data.map( character => (
<Card {...character} key={character.id}/>
))
}
</div>
)
})
Вот как будет выглядеть наш компонент App.tsx
.
Мы создаем функцию showData и оцениваем:
- Если свойство loading равно true, мы возвращаем компонент
<Loading/>
. - Если свойство error равно true, мы возвращаем компоненту
<ErrorMessage/>
, отправляя ошибку в компонент. - Если ни одно из условий не выполняется, это означает, что данные API готовы, и мы возвращаем компонент
<LayoutCards/>
и отправляем данные на отображение.
Наконец, под компонентом мы раскрываем скобки и вызываем функцию showData.
import { ErrorMessage, Header, Loading, LayoutCards } from './components'
import { useFetch } from './hook';
const App = () => {
const { data, loading, error } = useFetch();
const showData = () => {
if (loading) return <Loading/>
if (error) return <ErrorMessage msg={error}/>
return <LayoutCards data={data} />
}
return (
<>
<Header/>
{ showData() }
</>
)
}
export default App;
ПРИМЕЧАНИЕ: Вы также можете перенести функцию showData в хук и изменить расширение файла хука на .tsx
, это связано с тем, что вы используете JSX при возврате различных компонентов.
Спасибо, что дошли так далеко. 🙌
Я оставлю репозиторий, чтобы вы могли взглянуть на него, если захотите. ⬇️
Franklin361 / fetching-data-custom-hook
Руководство по получению данных и созданию пользовательского хука
Получение данных и создание пользовательских Hook
Руководство по получению данных и созданию пользовательского хука