原文地址:robinwieruch 全文使用意译,不是重要的我就没有翻译了前端
在本教程中,我想向你展现如何使用 state 和 effect 钩子在React中获取数据。 你还将实现自定义的 hooks 来获取数据,能够在应用程序的任何位置重用,也能够做为独立节点包在npm上发布。react
若是你对 React 的新功能一无所知,能够查看 React hooks 的相关 api 介绍。若是你想查看完整的如何使用 React Hooks 获取数据的项目代码,能够查看 github 的仓库ios
若是你只是想用 React Hooks 进行数据的获取,直接 npm i use-data-api
并根据文档进行操做。若是你使用他,别忘记给我个star 哦~git
注意:未来,React Hooks 不适用于 React 中获取数据。一个名为Suspense的功能将负责它。如下演练是了解React中有关 state 和 Effect hooks 的更多信息的好方法。github
若是您不熟悉React中的数据提取,请查看我在React文章中提取的大量数据。 它将引导您完成使用React类组件的数据获取,如何使用Render Prop 组件和高阶组件来复用这些数据,以及它如何处理错误以及 loading 的。npm
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 组件显示了一个项目列表(hits=Hacker News 文章)。状态和状态更新函数来自useState 的 hook。他是来负责管理咱们这个 data 的状态的。userState 中的第一个值是data 的初始值。其实就是个解构赋值。redux
这里咱们使用 axios 来获取数据,固然,你也可使用别的开源库。axios
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://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 的 effect hook 来获取数据。而且使用 useState 中的 setData 来更新组件状态。api
可是如上代码运行的时候,你会发现一个特别烦人的循环问题。effect hook 的触发不只仅是在组件第一次加载的时候,还有在每一次更新的时候也会触发。因为咱们在获取到数据后就进行设置了组件状态,而后又触发了 effect hook。因此就会出现死循环。很显然,这是一个 bug!咱们只想在组件第一次加载的时候获取数据 ,这也就是为何你能够提供一个空数组做为 useEffect
的第二个参数以免在组件更新的时候也触它。固然,这样的话,也就是在组件加载的时候触发。数组
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://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;
复制代码
第二个参数能够用来定义 hook 所依赖的全部变量(在这个数组中),若是其中一个变量发生变化,则就会触发这个 hook 的运行。若是传递的是一个空数组,则仅仅在第一次加载的时候运行。
是否是感受 ,干了
shouldComponentUpdate
的事情
这里还有一个陷阱。在这个代码里面,咱们使用 async/await
去获取第三方的 API 的接口数据,根据文档,每个 async
都会返回一个 promise:async
函数声明定义了一个异步函数,它返回一个 AsyncFunction 对象。异步函数是经过事件循环异步操做的函数,使用隐式的 Promise 返回结果然而,effect hook 不该该返回任何内容,或者清除功能。这也就是为啥你看到这个警告:
07:41:22.910 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
中使用 async
的缘由。可是咱们能够经过以下方法解决:
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(
'https://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 数据。可是,若是你对错误处理、loading、如何触发从表单中获取数据或者如何实现可重用的数据获取的钩子。请继续阅读。
目前咱们已经经过组件第一次加载的时候获取了接口数据。可是,如何可以经过输入的字段来告诉 api 接口我对那个主题感兴趣呢?(就是怎么给接口传数据。这里原文说的有点啰嗦(还有 redux 关键字来混淆视听),我直接上代码吧)...
...
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
的第二个参数是一个空数组,因此effect hook 的触发不依赖任何变量,所以只在组件第一次加载的时候触发。因此这里咱们但愿当 query 这个字段一改变的时候就触发搜索
...
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;
复制代码
如上,咱们只是把 query
做为第二个参数传递给了 effect hook,这样的话,每当 query 改变的时候就会触发搜索。可是,这样就会出现了另外一个问题:每一次的query 的字段变更都会触发搜索。如何提供一个按钮来触发请求呢?
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(
'https://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>
);
}
复制代码
这是一个使用 effect hook 来获取数据的一个例子,你能够决定 effect hook 因此依赖的状态。一旦你点击或者其余的什么操做 setState 了,那么 effect hook 就会运行。可是这个例子中,只有当你的 url 发生变化了,才会再次去获取数据。
这里让咱们来给程序添加一个 loading(加载器),这里须要另外一个 state
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(
'https://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;
复制代码
代码比较简单,不解释了
如何在 Effect Hook 中作一些错误处理呢?错误仅仅是一个 state ,一旦程序出现了 error state,则组件须要去渲染一些feedback 给用户。当咱们使用 async/await
的时候,咱们可使用try/catch
,以下:
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(
'https://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]);
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>
{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;
复制代码
每一次 effect hook 运行的时候都须要重置一下 error state,这是很是有必要的。由于用户可能想再发生错误的时候想再次尝试一下。
说白了,界面给用户反馈更加的友好
function App() {
...
return (
<Fragment>
<form onSubmit={event => {
setUrl(`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>}
...
</Fragment>
);
}
复制代码
为了防止浏览器的 reload,咱们这里加了一个event.preventDefalut()
,而后别的操做就是正常表单的操做了
其实就是请求的封装
为了可以提取自定义的请求 hook,除了属于输入框的 query 字段,别的包括 loading 加载器、错误处理函数都要包括在内。固然,你须要确保 App Component 所需的全部字段在你自定义的 hook 中都有返回
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'https://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]);
return [{ data, isLoading, isError }, setUrl];
}
复制代码
如今,咱们能够将你的新 hook 继续放到组件中使用
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>
);
}
复制代码
一般咱们须要一个初始状态。将它简单的传递给自定义 hook 中
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]);
return [{ data, isLoading, isError }, setUrl];
};
function App() {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, doFetch] = useDataApi(
'https://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;
复制代码
如上,就是咱们使用自定义 hook 来获取数据,该 hook 自己对 API 一无所知,它从外部接受全部的参数,可是仅管理重要的字段,好比 data、loading、error handler 等。它执行请求而且返回组件所须要的所有数据。
目前为止,咱们使用各类 state hook 来管理数据、loading、error handler 等。然而,全部的这些状态,经过他们本身的状态管理,都属于同一个总体,由于他们所关心的数据状态都是请求相关的。正如你所看到的,他们都在 fetch 函数中使用。他们属于同一类型的另外一个很好的表现就是在函数中,他们是一个接着一个被调用的(好比:setIsError、setIsLoading)。让咱们用一个 Reducer Hook 来将这三个状态结合起来!
一个 Reducer Hook 返回一个状态对象和一个改变状态对象的函数。这个函数就是 dispatch function:带有一个 type 和参数的 action。
其实这些概念跟 redux 一毛同样
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将reducer函数和初始状态对象做为参数。 在咱们的例子中,数据,加载和错误状态的初始状态的参数没有改变,但它们已经聚合到一个由 reducer hook 而不是单个state 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]);
...
};
复制代码
如今,在获取数据的时候,可使用 dispathc function
来给reducer
传递参数。使用dispatch函数发送的对象具备必需的type属性和可选的payload属性。该类型告诉reducer功能须要应用哪一个状态转换,而且reducer能够另外使用有效负载来提取新状态。毕竟,咱们只有三个状态转换:初始化提取过程,通知成功的数据提取结果,并通知错误的数据提取结果。
在咱们自定义的 hook 中,state 像之前同样返回。可是由于咱们有一个状态对象而不是独立状态。 这样,调用useDataApi自定义钩子的人仍然能够访问数据,isLoading和isError:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
return [state, setUrl];
};
复制代码
最后还有咱们 reducer 函数的实现。它须要做用于三个不一样的状态转换,称为FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE。 每一个状态转换都须要返回一个新的状态对象。 让咱们看看如何使用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 都有对应的处理,而且返回一个新的 state。
总之,Reducer Hook确保状态管理的这一部分用本身的逻辑封装。此外,你永远不会遇到无效状态。例如,之前可能会意外地将isLoading和isError状态设置为true。 在这种状况下,UI应该显示什么?如今,reducer函数定义的每一个状态转换都会致使一个有效的状态对象。
React中的一个常见问题是,即便组件已经卸载(例如因为使用React Router导航),也会设置组件状态。我以前已经在这里写过关于这个问题的文章,它描述了如何防止在各类场景中为未加载的组件中设置状态。 让咱们看看咱们如何阻止在数据提取的自定义钩子中设置状态:
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]);
return [state, setUrl];
};
复制代码
每个 Effect Hook 都自带一个清理功能。该功能在组件卸载时运行。清理功能是 hook 返回的一个功能。在咱们的例子中,咱们使用一个名为 didCancel
的 boolean 来标识组件的状态。若是组件已卸载,则该标志应设置为true,这将致使在最终异步解析数据提取后阻止设置组件状态。
注意:实际上不会停止数据获取 - 这能够经过Axios Cancellation实现 - 可是对于 unmounted
的组件再也不执行状态转换。 因为Axios Cancellation在我看来并非最好的API,所以这个防止设置状态的布尔标志也能完成这项工做。
关注公众号: 【全栈前端精选】 每日获取好文推荐。还能够入群,一块儿学习交流呀~~