В этом руководстве я хочу показать вам, как использовать перехватчики состояний и эффектов для запроса данных в React. Мы будем использовать всю известнуюHacker News APIчтобы получить несколько популярных статей. Вы определите свои собственные хуки для запросов данных, которые можно повторно использовать во всех ваших приложениях или публиковать в npm.
Если вы не знакомы с этими новыми функциями React, вы можете ознакомиться с другой моей статьей.introduction to React Hooks. Если вы хотите увидеть пример статьи напрямую, вы можете проверить это напрямую.Репозиторий на гитхабе.
Примечание. В будущих версиях React хуки больше не будут использоваться для извлечения данных, вместо этого будет использоваться метод, называемый
Suspense
с вещами. Несмотря на это, следующие методы по-прежнему являются хорошим способом понять хуки STATE и EFFECT.
Использование React Hooks для запросов данных
Если у вас нет опыта работы с запросами данных в React, вы можете прочитать мою статью:How to fetch data in React. В статье объясняется, как использовать компоненты класса для получения данных, как использовать повторно используемые компоненты Render Props и компоненты более высокого порядка, а также как обрабатывать состояния обработки ошибок и загрузки. В этой статье я хочу воспроизвести все это, используя компоненты Function и React Hooks.
import React, { useState } from 'react';
function App() {
const [data, setData] = useState({ hits: [] });
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
Компонент приложения отобразит список из статей Hacker News. Функции состояния и обновления состояния будут вызыватьсяuseState
Генерируется обработчик состояния, который отвечает за управление локальным состоянием компонента приложения, полученного через запрос. Исходное состояние — это пустой массив, и в данный момент негде установить для него новое состояние.
Мы будем использовать axios для извлечения данных, конечно, вы также можете использовать знакомую вам библиотеку запросов или API выборки, который поставляется с вашим браузером. Если у вас не установлен axios, вы можете пройтиnpm install axios
установить.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
мы вuseEffect
В этом хуке эффекта данные получаются из API через axios, а функция обновления хука состояния используется для хранения данных в локальном состоянии. И используйте async/await для разрешения промисов.
Однако, когда вы запускаете приведенный выше код, вы застреваете в чертовом бесконечном цикле. Хук эффекта выполняется, когда компонент монтируется и обновляется. Поскольку мы обновляем состояние каждый раз, когда получаем данные, компонент обновляется и снова запускает эффект, который снова и снова запрашивает данные. Очевидно, нам нужно избегать таких ошибок,Мы хотим запрашивать данные только тогда, когда компонент смонтирован.. Вы можете передать пустой массив во втором параметре, предоставляемом хуком эффекта, что позволит избежать выполнения хука эффекта при обновлении компонента, но компонент все равно будет выполнять его во время монтирования.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
Второй параметр используется для определения переменных, от которых зависит хук. Хук запустится автоматически, если изменится одна из переменных. Если вторым параметром является пустой массив, то хук не будет запускаться при обновлении компонента, потому что он не отслеживает какие-либо переменные.
Есть еще один момент, требующий особого внимания: в коде мы используем async/await для получения данных, предоставляемых сторонним API. Согласно документации, каждая асинхронная функция возвращает неявное обещание:
"The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. "
«Асинхронная функция определяет асинхронную функцию, которая возвращает объект асинхронной функции. Асинхронная функция — это функция, которая работает через цикл событий, используя неявное обещание для возврата конечного результата».
Однако обработчик эффекта должен либо ничего не возвращать, либо возвращать функцию очистки. Вот почему вы видите сообщение об ошибке в консоли.
index.js:1452 Warning: useEffect function must return a cleanup function or nothing.
Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.
Это означает, что мы не можем напрямуюuseEffect
Функции используют асинхронность. Давайте реализуем решение, позволяющее использовать асинхронные функции в обработчиках эффектов.
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
Это небольшой пример использования React Hooks для запросов данных. Однако, если вас интересует обработка ошибок, состояния загрузки, запуск извлечения данных из формы и повторное использование ловушек для обработки задач, давайте двигаться дальше.
Как активировать хук вручную или автоматически?
Теперь мы смогли получить данные после монтирования компонента, однако, подскажите, как использовать поле динамического ввода API для выбора интересующей его темы? Прежде чем вы сможете увидеть код, мы по умолчанию используем «Redux» в качестве параметров запроса ( 'Тем не менее, Aragorn.com/API/V1/sear…Как насчет тем, связанных с React? Давайте реализуем поле ввода, которое может получать темы, отличные от «Redux». Теперь давайте введем новое состояние для поля ввода.
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
export default App;
Теперь два состояния данных запроса и параметры запроса независимы друг от друга, но нам нужен способ надеяться, что они связаны, и получить только статью темы, указанную параметром, введенным в поле ввода. Со следующими изменениями компонент должен следовать запросу, чтобы получить соответствующую статью после монтирования.
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, []);
return (
...
);
}
export default App;
На самом деле, нам все еще не хватает частей кода. Вы обнаружите, что когда вы вводите содержимое в поле ввода, новые данные не получаются. Это связано с тем, что второй параметр useEffect — это просто пустой массив, эффект не зависит ни от каких переменных, поэтому он сработает только один раз во время монтирования. Однако теперь нам нужно полагаться на условие запроса, и как только отправка запроса изменится, запрос данных должен сработать снова.
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
...
);
}
export default App;
Что ж, теперь, как только вы измените содержимое поля ввода, данные будут получены снова. Но теперь есть другая проблема: каждый раз, когда вводится новый символ, срабатывает эффект, чтобы сделать новый запрос. Так что, если мы предоставим кнопку для ручного запуска запроса данных?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${search}`,
);
setData(result.data);
};
fetchData();
}, [search]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setSearch(query)}>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
Кроме того, начальное состояние состояния поиска также устанавливается в то же состояние, что и состояние запроса, потому что компонент будет запрашивать данные один раз при его монтировании, и результат в это время также должен отражать условия поиска в поле ввода. . Однако состояние поиска и состояние запроса имеют схожие значения, что может сбивать с толку. Почему бы просто не установить реальный URL-адрес в состояние поиска?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData(result.data);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}
Это относится к выборке данных через хуки эффектов, вы можете решить, от какого состояния зависит эффект. В этом случае, если состояние URL-адреса изменится, снова запустите эффект, чтобы обновить статью темы через API.
Состояние загрузки и React Hooks
Давайте введем состояние загрузки во время загрузки данных. Это просто еще одно состояние, управляемое хуком состояния. Состояние загрузки используется для отображения состояния загрузки в компоненте приложения.
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
Search
</button>
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default App;
Теперь, когда компонент находится в состоянии монтирования или состояние URL-адреса изменено, эффект вызывается для получения данных, и состояние загрузки становится истинным. После завершения запроса состояние загрузки снова становится ложным.
Обработка ошибок и хуки React
Как обрабатывать ошибки при запросе данных через React Hooks? Ошибка — это просто еще одно состояние, инициализированное с помощью обработчика состояния. В случае возникновения ошибки компонент приложения может сообщить об этом пользователю. При использовании асинхронных/ожидающих функций обычно используют try/catch для перехвата ошибок.Вы можете сделать следующее в эффекте:
...
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
...
{isError && <div>Something went wrong ...</div>}
...
<Fragment>
);
Эффект сбрасывает состояние ошибки при каждом запуске, что полезно, поскольку после каждого неудачного запроса пользователь может повторить попытку, что сбрасывает ошибку. Чтобы убедиться, что код работает, вы можете ввести бесполезный URL-адрес и проверить, появляется ли сообщение об ошибке.
Получение данных с помощью форм
Какая правильная форма для получения данных? Прямо сейчас у нас есть только элементы ввода и кнопки для объединения, как только будут введены дополнительные элементы ввода, вы можете захотеть обернуть их формами. Кроме того, форма также может вызывать событие «Ввод» клавиатуры.
function App() {
...
const doFetch = (evt) => {
evt.preventDefault();
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
}
return (
<Fragment>
<form
onSubmit={ doFetch }
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
...
</Fragment>
);
}
Пользовательский хук для получения данных
Мы можем определить собственный хук для извлечения всего, что связано с запросом данных, в дополнение к состоянию запроса поля ввода, в дополнение к состоянию загрузки и обработке ошибок. Также не забудьте вернуть переменные, которые необходимо использовать в компоненте.
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return { data, isLoading, isError, doFetch };
}
Теперь мы используем наш новый хук в компоненте App.
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
...
</Fragment>
);
}
Затем передайте URL-адрес извне вDoFetch
метод.
const useHackerNewsApi = () => {
...
useEffect(
...
);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
<form
onSubmit={event => {
doFetch(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
event.preventDefault();
}}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
...
</Fragment>
);
}
Начальное состояние также является общим и может быть просто передано в пользовательские хуки через параметры:
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useDataApi(
'http://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] },
);
return (
<Fragment>
<form
onSubmit={event => {
doFetch(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
event.preventDefault();
}}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default App;
Вот как можно получить данные с помощью пользовательского хука, сам хук ничего не знает об API, он получает параметры извне, управляет только необходимым состоянием, таким как данные, загрузка и состояние, связанное с ошибкой, и выполняет запрос и передает данные через хук Возврат к компоненту.
Редукционный хук для получения данных
До сих пор мы использовали обработчики состояния для управления полученными данными, статусом загрузки и статусом ошибки. Тем не менее, все состояния имеют свои собственные хуки состояний, но все они связаны друг с другом и заботятся об одном и том же. Как видите, все они используются в функции выборки данных. Они вызываются один за другим (например:setIsError
,setIsLoading
), что является правильным способом соединения их вместе. Давайте соединим все три вместе с помощью редуктора.
Reducer Hook возвращает объект состояния и функцию (для изменения объекта состояния). Эта функция называется функцией отправки и отправляет действие с двумя свойствами: типом и полезной нагрузкой. Вся эта информация поступает в функцию редуктора, которая извлекает новое состояние на основе предыдущего состояния. Давайте посмотрим, как это работает в коде:
import React, {
Fragment,
useState,
useEffect,
useReducer,
} from 'react';
import axios from 'axios';
const dataFetchReducer = (state, action) => {
...
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
};
Reducer Hook принимает функцию-редуктор и объект начального состояния в качестве параметров. В нашем случае загруженные данные, состояние загрузки и состояние ошибки используются в качестве параметров начального состояния и не изменяются, но они объединяются в объект состояния, управляемый обработчиками редюсеров, а не отдельными обработчиками состояния.
const dataFetchReducer = (state, action) => {
...
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData();
}, [url]);
...
};
Теперь при выборке данных вы можете использовать функцию отправки для отправки информации в функцию редуктора. Объекты, отправляемые с помощью функции отправки, имеют обязательноеtype
атрибуты и необязательныйpayload
Атрибуты. Свойство type сообщает функции редуктора, какое состояние ей нужно преобразовать, а также может извлекать новое состояние из полезной нагрузки. Здесь есть только три перехода состояния: инициализация процесса данных, уведомление о результате успешного запроса данных и уведомление о результате неудачного запроса данных.
В конце пользовательского хука состояние возвращается, как и раньше, но поскольку все наше состояние находится в одном объекте, а не в отдельных состояниях, объект состояния разрушается и возвращается. Таким образом, вызываяuseDataApi
Люди с нестандартными крючками по-прежнему могутdata
,isLoading
а такжеisError
:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
Наконец, нам не хватает реализации функции редуктора. Он должен обрабатывать три разных перехода состояний, которые называютсяFEATCH_INIT
,FEATCH_SUCCESS
,FEATCH_FAILURE
. Каждый переход состояния должен возвращать новое состояние. Давайте посмотрим, как реализовать эту логику с помощью switch case:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return { ...state };
case 'FETCH_SUCCESS':
return { ...state };
case 'FETCH_FAILURE':
return { ...state };
default:
throw new Error();
}
};
Функция редуктора может получить доступ к текущему состоянию и отправить входящее действие через свои аргументы. До сих пор в операторах switch case каждый переход состояния просто возвращал предыдущее состояние, а операторы деструктора использовались для сохранения неизменности объекта состояния (т. е. состояние никогда не изменялось напрямую). Теперь давайте переопределим некоторые свойства, возвращаемые текущим состоянием, чтобы изменять некоторое состояние при каждом переходе:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false
};
case 'FETCH_SUCCESS':
return {
...state,
isLoading: false,
isError: false,
data: action.payload,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
default:
throw new Error();
}
};
Теперь каждый переход состояния (определяемый action.type) возвращает новое состояние на основе предыдущего состояния и необязательную полезную нагрузку. Например, в случае успешного запроса полезная нагрузка используется для установки свойства данных нового объекта состояния.
Таким образом, хук-редюсер гарантирует, что эта часть управления состоянием инкапсулирована собственной логикой. Предоставляя тип действия и дополнительную полезную нагрузку, вы всегда получаете предсказуемые изменения состояния. Кроме того, недопустимое состояние никогда не встречается. Например, можно было случайно поставитьisLoading
а такжеisError
Установите значение «истина». Что в этом случае должно отображаться в пользовательском интерфейсе? Теперь каждый переход состояния, определенный функцией редуктора, указывает на действительный объект состояния.
Прервать запрос данных в эффект-хуке
В React распространенной проблемой является то, что состояние компонента все еще назначается даже после того, как компонент был размонтирован. Об этой проблеме я писал в предыдущем посте, в котором описываетсяКак предотвратить установку состояния для несмонтированных компонентов в различных сценариях. Давайте посмотрим, как предотвратить настройку состояния при запросе данных в пользовательском хуке:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
if (!didCancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
}
} catch (error) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
Каждый эффект-хук поставляется с функцией очистки, которая запускается, когда компонент выгружается. Функция очистки — это функция, возвращаемая хуком. В этом случае мы используемdidCancel
переменная, чтобы позволитьfetchData
Знать состояние компонента (монтирование/размонтирование). Если компонент действительно размонтирован, флаг должен быть установлен вtrue
, что предотвращает установку состояния компонента после окончательной выборки данных асинхронного синтаксического анализа.
Примечание. Извлечение данных на самом деле не прерывается (хотя это можно сделать с помощью отмены Axios), но переходы между состояниями больше не выполняются для несмонтированных компонентов. Поскольку отмена Axios, на мой взгляд, не лучший API, этот логический флаг, который предотвращает установку состояния, также подойдет.