在本教程中,我会向您展示如何使用**state ****和 ****effect **在React with Hooks中获取数据。我们将使用Hacker News API从科技界获取热门文章。您还将实现用于数据获取的自定义hook,该hook 可在应用程序中的任何位置重用或作为独立节点程序包发布在npm上。
如果您对这个新的React功能一无所知,请查看React Hooks简介。
使用 react hook请求数据
如果您不熟悉React中的数据获取,请查看我的这篇文章。 它会带您了解**如何使用React类组件获取数据,如何使用**** Render Prop Components 和 Higher-Order Components,**使其可重用,以及如何处理错误和处理loading状态。 在本文中,我想通过 函数式组件中的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;
在App组件中,显示了一个list(hits= Hacker News文章)。 状态和状态更新功能来自名为useState
的hook,该hook负责管理App组件获取的数据的本地状态。 初始状态是空白列表。
我们将使用axios来获取数据,但是由您决定使用另一个数据获取库或浏览器的本机获取API。 如果尚未安装axios,则可以在命令行上使用npm install axios进行安装。 然后为数据获取实现效果挂钩:
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await fetch('https://hn.algolia.com/api/v1/search?query=redux');
const resOjson = await result.json();
setData(resOjson)
})
return (<>
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>);
}
使用useEffect这个钩子函数从 API获取数据,使用useState这个钩子函数将获得数据,存储为state。
然而,在运行代码时,代码会陷入一个讨厌的循环。useEffect函数在组件安装时执行,但在组件更新时也会执行**。
因为我们在每次获取数据后都会设置状态(state),所以组件会更新。这就会导致useEffect函数再次执行。 它一次又一次地获取数据。那么怎么避免这个bug呢–我们只需要在组件安装时获取数据。 可以为****useEffect提供一个空数组作为第二个参数,以避免在组件更新时激活它,而仅在安装组件时激活它。**
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await fetch('https://hn.algolia.com/api/v1/search?query=redux');
const resOjson = await result.json();
setData(resOjson)
}, [])
return (<>
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>);
}
代码会出现如下警告
Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately:
useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // … } fetchData(); }, [someId]); // Or [] if effect doesn’t need props or state
Learn more about data fetching with Hooks: fb.me/react-hooks… in Unknown (created by Hello) in div (created by Hello) in Hello
useEffect的第二个参数是一个数组类型的变量,这个变量表示 useEffct所依赖的所有变量集合。如果数组中任意一个变量变化,则 useEffect会再次执行。如果变量为空数组,则在组件更新时useEffect不会再次执行,因为他不必依赖任何变量
在代码中,我们使用async / await从第三方API获取数据, 根据MDN文档-async,描述
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.
async function
** 用来定义一个返回AsyncFunction
对象的异步函数,异步函数是指通过事件循环****异步执行的函数,它会通过一个隐式的Promise
返回其结果**
但是我们的代码中,异步 useEffect函数没有返回任何内容,这就是为什么在控制台中会出现上面加粗红色的警告内容–
**useEffect函数必须返回清除函数,或者不返回任何内容。如果你想将 useEffect 写成异步函数,或者返回一个Promise, 你可以在 **useEffect中 再申明一个异步函数并立刻调用它
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await fetch('https://hn.algolia.com/api/v1/search?query=redux');
const resOjson = await result.json();
setData(resOjson)
}
fetchData();
}, [])
return (<>
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>);
}
以上就是在react的函数式组件中获取数据的方法了,但是,如果你对 如何在hooks组件中处理 请求错误,处理loading状态,如何从表单中获取数据 以及 如何重用hooks函数组件感兴趣,请继续阅读
如何触发 hooks函数
我们已经可以在一个hooks函数中 处理数据请求的情况了,但是怎么给请求传递参数呢?
让我们来实现一个带输入框,并给接口 传递参数的组件
我们通过 (data
,query
)这两种状态,实现输入框改变的时候查询指定的数据(给useEffect函数传递第二个参数来实现这个功能 — react官方useEffect文档)
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await fetch(`https://hn.algolia.com/api/v1/search?query=${query}`);
const resOjson = await result.json();
setData(resOjson)
}
fetchData();
}, [query])
return (<>
<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>
</>);
}
更改输入框的值后,就可以重新请求数据了。 但这带来了另一个问题:在输入框中每改变一个字符,都会触发数据请求。
输入框频繁触发请求-解决方式一
提供一个触发请求的按钮,从而手动触发钩子?
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await fetch(`https://hn.algolia.com/api/v1/search?query=${search}`);
const resOjson = await result.json();
setData(resOjson)
}
fetchData();
}, [search])
return (<>
<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>
</>);
}
search
的初始状态也被设置为与query
相同的状态–因为该组件还需要在第一次记加载完成时获取数据。 但是,具有类似的 search和query状态有点令人困惑。 为什么不将实际URL设置为状态来替换 search
?
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
`https://hn.algolia.com/api/v1/search?query=redux`,
);
useEffect(() => {
const fetchData = async () => {
const result = await fetch(url);
const resOjson = await result.json();
setData(resOjson)
}
fetchData();
}, [url])
return (<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setUrl(`https://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>
</>);
}
输入框频繁触发请求-解决方式二
使用节流函数处理
在 hooks组件请求中使用loading状态
大多数场景中,数据处于请求中时应该出现一个加载的状态 。 在hooks组件中,这个加载状态也可以存储为一个state。
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
`https://hn.algolia.com/api/v1/search?query=redux`,
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await fetch(url);
const resOjson = await result.json();
setData(resOjson);
setIsLoading(false);
}
fetchData();
}, [url])
return (<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setUrl(`https://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>
)}
</>);
}
在React Hooks中捕获错误
当请求出现错误时,在react hooks中该如何处理呢? —- 一旦出现错误,App组件应立即为用户提供反馈。 我们通常使用try / catch块进行错误处理。
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
`https://hn.algolia.com/api/v1/search?query=redux`,
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setIsError(null);
setIsLoading(true);
try {
const result = await fetch(url);
const resOjson = await result.json();
// 故意写错
a = a + f;
setData(resOjson);
} catch (error) {
setIsError(`${error}`);
}
setIsLoading(false);
}
fetchData();
}, [url])
const renderList = () => {
if (isError) {
return <div>{isError}</div>
}
if (isLoading) {
return <div>Loading ...</div>
}
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)
}
return (<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>
Search
</button>
{renderList()}
</>);
}
每次useEffect
运行时,都会重置错误状态。 这很有用,因为在失败的请求之后,用户可能想要再次尝试,这将重置错误。 您可以将URL更改为无效的内容。 然后检查是否显示错误消息。
在React Form中请求数据
在表单中如何获取数据呢?到目前为止,我们只有输入字段和按钮的组合。 引入更多输入元素后,您可能需要用form元素包裹它们。 而且,form表单还可以通过键盘上的“ Enter”来触发提交操作。
import React, { useState, useEffect, useReducer } from 'react'
export default props => {
const { } = props;
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
`https://hn.algolia.com/api/v1/search?query=redux`,
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setIsError(null);
setIsLoading(true);
try {
const result = await fetch(url);
const resOjson = await result.json();
// a = a + f;
setData(resOjson);
} catch (error) {
setIsError(`${error}`);
}
setIsLoading(false);
}
fetchData();
}, [url])
const renderList = () => {
if (isError) {
return <div>{isError}</div>
}
if (isLoading) {
return <div>Loading ...</div>
}
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)
}
return (<>
<form
onSubmit={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{renderList()}
</>);
}
但是现在,单击“提交”按钮时,浏览器将重新加载,因为这是浏览器提交表单时的原生行为。 为了阻止默认提交,我们可以在React事件上调用一个函数。
....
<form onSubmit={(event) => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
event.preventDefault();
}
}>
.....
用Hooks自定义请求
import React, { useState, useEffect, useReducer } from 'react'
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setIsError(null);
setIsLoading(true);
try {
const result = await fetch(url);
const resOjson = await result.json();
setData(resOjson);
} catch (error) {
setIsError(`${error}`);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
export default useDataApi;
import React, { useState, useEffect, useReducer } from 'react'
import useDataApi from './useDataApi'
export default props => {
const { } = props;
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, doFetch] = useDataApi(
'https://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] },
);
const renderList = () => {
if (isError) {
return <div>{isError}</div>
}
if (isLoading) {
return <div>Loading ...</div>
}
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)
}
return (<>
<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>
{renderList()}
</>);
}
自定义数据请求的Hooks已完成。 自定义的请求hooks本身对API一无所知。 它从外部接收所有参数,并且仅管理必要的状态,例如数据,加载和错误状态。 它执行请求,并将数据用作自定义数据获,然后将数据返回给组件。
在Hooks中终止请求
在React中,一个常见的问题是即使组件已经被卸载(例如,由于使用React Router导航),也会设置组件状态。 我之前在这里已经写过关于此问题的文章,它描述了如何在各种情况下防止未安装组件的设置state。 让我们看看如何防止在自定义钩子中为数据获取设置状态:
import React, { useState, useEffect, useReducer } from 'react'
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(null);
useEffect(() => {
// 主要是这个变量
let didCancel = false;
const fetchData = async () => {
setIsError(null);
setIsLoading(true);
try {
const result = await fetch(url);
const resOjson = await result.json();
if (!didCancel) {
setData(resOjson);
}
} catch (error) {
if (!didCancel) {
setIsError(`${error}`);
}
}
setIsLoading(false);
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};