原文:www.robinwieruch.de/react-hooks…javascript
原做者:Robin Wieruchhtml
在本文中,我将会向你展现在React中怎样用Hooks来获取数据经过使用state和effect hooks。咱们将用众所周知的Hacker News API来获取科技界的热门文章。你也能够实现获取数据的自定义hook,在应用的任何位置复用,也能够做为独立的依赖包在npm上发布。java
若是你对这个React的新功能一无所知,请查看个人另外一篇文章 introduction to React Hooks。若是你想查看直接查看文章的示例,请查看此Github仓库。react
提示:在未来的版本中,React Hooks不适用于在React中获取数据。取而代之的是一个叫作Suspense
的功能。尽管如此,下面的练习依然是了解 state 和 effect 两种 Hooks 的好方法。ios
若是你不熟悉在React中获取数据,能够阅读个人文章:How to fetch data in React。文章将讲解如何使用class components获取数据,如何复用Render Prop Components和Higher-Order Components,以及如何进行错误处理和 loading 状态。在本文中,我会在function components中使用React Hooks来从新实现这些功能。git
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 articles 。状态和状态更新函数将经过 useState
的hook来生成,它将负责管理hits列表数据的本地状态。初始状态是一个空数组,此时尚未为其设置任何的状态。github
咱们将使用axios来获取数据,固然你也可使用其余的库或者浏览器的原生fetch API,若是你还没安装axios,你能够在命令行使用npm install axios
来安装。而后来实现用于数据获取的effect hook:npm
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;
复制代码
经过axios调用API在useEffect
这个 effect hook中获取数据,而后经过setData将数据放到组件本地的state中,而后经过async/await来处理Promise。redux
可是,当你运行应用程序时,你会遇到一个讨厌的循环。由于effect hook不只在组件挂载是执行,在组件更新过程当中也会执行。由于咱们在每一次的数据获取后都会从新设置状态,这时候组件update而后effect hook就会从新运行一遍,这就形成了数据一次又一次的获取。咱们只想在组件挂载阶段时获取数据。这就是你要给effect hook的第二个参数传入一个空数组的缘由,这样作能够避免组件更新阶段执行 effect hook ,可是依然会在挂载阶段执行它。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;
复制代码
第二个参数用来定义hooks所依赖的所有变量(存放在数组中),若是一个变量改变了,effect hook就会被执行一次,若是是一个空数组的话,hooks将不会在组件更新的时候执行,由于它没有监听到任何的变量。
还有最后一个问题,在代码中,咱们使用了 async/await 来获取第三方 API 提供的数据。根据文档,每个 async 函数都将返回一个隐式的 promise:“async 函数定义了一个异步函数,它返回的是一个AsyncFunction
对象,异步函数是一个经过事件循环进行操做的函数,使用隐式的 Promise 返回最终的结果。”。
可是,effect hook应该不返回任何内容或返回一个clean up函数,这就是为何你会在控制台看到如下警告: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的缘由。让咱们经过在effect内部使用异步函数来实现它的解决方案。
import ...
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 ...;
}
export default App;
复制代码
这就是用React Hooks获取数据的小🌰。可是,若是你对错误处理、loading状态、如何从表单中触发数据获取以及如何复用数据获取的hook感兴趣,请继续阅读。
好的,咱们在组件挂载后获取了一次数据。可是如何使用输入字段告诉API咱们感兴趣的主题?“Redux”作为默认查询。可是若是想要查询关于“React”的呢?让咱们实现一个input元素,能够得到“Redux”以外的话题。所以,就要为input元素引入一个新的state。
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
...
}, []);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
...
</ul>
</Fragment>
);
}
export default App;
复制代码
如今,请求数据和查询参数这两个 state 相互独立,可是咱们但愿将它们耦合起来,以获取输入框输入的参数指定的话题文章。经过如下修改,组件应该在挂载后按照查询参数获取相应文章。
...
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;
复制代码
如今还差一部分:当你在input中输入一些内容时,在挂载后就不会再获取任何数据了,由于咱们提供了[]
做为第二个参数,effect没有依赖任何变量,所以只会在挂载阶段触发,可是如今的effect应该依赖query
,每当query
改变的时候,就应该从新获取数据。
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
...
}, [query]);
return (
...
);
}
export default App;
复制代码
好了,如今input的值改变就会从新获取数据。可是又出现另一个问题:每次输入一个新字符,就会触发 effect 进行一次新的请求。那么咱们如何提供一个按钮来手动触发数据请求呢?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
...
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setSearch(query)}>
Search
</button>
<ul>
...
</ul>
</Fragment>
);
}
复制代码
如今,effect的触发依赖于search,而不是随输入字段中变化的query。一旦用户点击按钮,新的search才会被设置,而且会手动触发effect hook。
...
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 (
...
);
}
export default App;
复制代码
此外,search的初始状态也被设置成了与 query相同,由于组件在挂载阶段会请求一次数据,此时的结果也应该反映的是输入框中的搜索条件。然而, search和 query有点相似,这看起来比较困惑。为何不将请求的实际URL设置到 search state 中呢?
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> ); } 复制代码
这就是经过 effect hook 获取数据的案例,你能够决定effect依赖于哪一个state。一旦在点击或其余effect中设置此state,此effect就会再次运行。在这种状况下,若是 URL 的 state 发生改变,则再次运行该effect经过 API 从新获取主题文章。
让咱们在数据的加载过程当中引入一个 Loading 状态。它只是另外一个由 state hook 管理的状态。Loading state 用于在 App 中渲染一个 Loading 状态。
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;
复制代码
一旦调用了effect获取了数据,(在组件挂载阶段或URL状态更改时发生),则加载状态设置为true。请求完成后,加载状态再次设置为false。
若是在React Hooks中进行错误处理呢,错误只是用state hook初始化的另外一个状态。一旦出现错误状态,应用程序组件就能够为用户显示反馈。使用async/await时,一般使用try/catch块进行错误处理。你能够在effect内作到:
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);
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;
复制代码
每次hook从新运行时,error state都会被重置。这会颇有用,由于每次请求失败后,用户可能从新尝试,这样就可以重置错误。为了观察代码是否生效,你能够填写一个无用的 URL ,而后检查错误信息是否会出现。
如今咱们只有输入框和按钮进行组合,一旦引入更多的 input 元素,你可能想要使用表单来进行包装。此外表单还可以触发键盘的 “Enter” 事件。
function App() {
...
return (
<Fragment>
<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>
{isError && <div>Something went wrong ...</div>}
...
</Fragment>
);
}
复制代码
可是如今浏览器在单击提交按钮时会从新加载,由于这是浏览器在提交表单时的默认行为。为了阻止默认行为,咱们能够经过event.preventDefault()取消默认行为。这和你在React Class组件中的实现方式相同。
function App() {
...
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return (
<Fragment>
<form onSubmit={event => {
doFetch();
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>
);
}
复制代码
如今,当你单击提交按钮时,浏览器不会再从新加载。它和之前同样工做,但此次使用的是表单,而不是简单的input和按钮组合。你也能够按键盘上的“回车”键。
咱们能够定义一个自定义的 hook,提取出全部与数据请求相关的东西,除了输入框的 query state,以及 Loading 状态、错误处理。还要确保返回组件中须要用到的变量。
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 };
}
复制代码
如今,您的hook能够再次在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>
);
}
复制代码
初始状态也能够是通用的。将它简单地传递给新的自定义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]);
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;
复制代码
这就是使用自定义 hook 获取数据的方法,hook 自己对API无感知,它从外部获取参数,只管理必要的 state ,如数据、 Loading 和error state ,而且执行请求并将数据经过 hook 返回给组件。
目前为止,咱们已经使用 state hooks 来管理了咱们获取到的数据、Loading 状态、error state。然而,全部的状态都有属于本身的 state hook,可是他们又都链接在一块儿,关心的是一样的事情。如你所见,全部的它们都在数据获取函数中被使用。它们一个接一个的被调用(好比:setIsError
、setIsLoading
),这才是将它们链接在一块儿的正确用法。让咱们用一个 Reducer Hook 将这三者链接在一块儿。
Reducer Hook 返回一个 state 对象和一个函数(用来改变 state 对象)。这个函数被称为分发函数(dispatch function),它分发一个 action,action 具备 type 和 payload 两个属性。全部的这些信息都在 reducer 函数中被接收,根据以前的状态提取一个新的状态。让咱们看看在代码中是如何工做的:
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函数和初始state做为参数。在咱们的例子中,数据,Loading和error state的初始状态的参数没有改变,但它们已经聚合到一个由一个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]);
...
};
复制代码
如今,在获取数据时,可使用 dispatch 函数向 reducer 函数发送信息。使用 dispatch 函数发送的对象具备一个必填的 type
属性和一个可选的 payload
属性。type 属性告诉 reducer 函数须要转换的 state 是哪一个,还能够从 payload 中提取新的 state。在这里只有三个状态转换:初始化数据过程,通知数据请求成功的结果,以及通知数据请求失败的结果。
在自定义 hook 的末尾,state 像之前同样返回,可是由于咱们全部的 state 都在一个对象中,而再也不是独立的 state ,因此 state 对象进行解构返回。这样,调用 useDataApi
自定义 hook 的人仍然能够 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 };
};
复制代码
最后,咱们还缺乏reducer函数的实现。它须要处理三个不一样的状态转换,称为FETCH_INIT
,FETCH_SUCCESS
和FETCH_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();
}
};
复制代码
reducer函数能够经过其参数访问当前状态和传入操做。到目前为止,在out case 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决定)都返回一个基于以前 state 和 payload 的新状态。例如,在请求成功的状况下,payload 用于设置新 state 对象的 data 属性。
总之,reducer hook 确保使用本身的逻辑封装状态管理的这一部分。经过提供 action type 和可选 payload ,老是会获得可预测的状态更改。此外,永远不会遇到无效状态。例如,之前可能会意外地将 isLoading
和 isError
设置为true。在这种状况下,UI中应该显示什么? 如今,由 reducer 函数定义的每一个 state 转换都指向一个有效的 state 对象。
在React中的有一个常见问题,即便组件已经卸载(例如使用React Router切换了路由),仍会设置组件的state。我以前已经在这里写过关于这个问题的文章,它描述了如何防止在已经Unmount的组件中调用setState。让咱们看看咱们如何阻止在数据提取的自定义钩子中设置状态:
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 };
};
复制代码
每一个Effect Hook都带有一个clean up函数,它在组件卸载时运行。clean up 函数是 hook 返回的一个函数。在该案例中,咱们使用 didCancel
变量来让 fetchData
知道组件的状态(挂载/卸载)。若是组件确实被卸载了,则应该将标志设置为 true
,从而防止在最终异步解析数据获取以后设置组件状态。
*注意:实际上并无停止数据获取(不过能够经过Axios Cancellation来实现),可是再也不为卸载的组件执行状态转换。因为 Axios 取消在我看来并非最好的API,因此这个防止设置状态的布尔标志也能够完成这项工做。