在本文中,我想要向你介绍怎样通过state
和effecthooks
在React
的Hooks
用法中获取数据。我们将在科技界使用广为人知的Hacker News API
来获取热门文章。你也可以使用你自己的方法来获取hooks data
,你可以在你的应用中复用或者发布到npm
的库中作为一个node
包。
如果你对React
的新特性一无所知,建议你先阅读该链接introduction to React Hooks。如果你想查看完整的关于如何在React hooks
中获取数据的例子,去这查看吧GitHub repository
如果你已经准备好使用React Hook
来获取数据了。在项目中运行npm install use-data-api
然后查看文档。好用的话给个star
哦~
提示:在将来,React Hooks
不是React
计划中用来获取数据的。相反,一个叫做Suspense
的新特性将会代替它。不过下面的内容是了解React
中state
和effect
的很好的方法。
通过React Hooks
获取数据
如果你对于在React
中获取数据不是很熟悉,可以查看我的以往文章。这篇文章会引导你了解在class
组件中获取数据,这篇文章会让你了解通过props
和高级组件的数据复用。解决错误和进度条loading
处理。在本文中,我会告诉你在ReactHooks
中实现以上用法,当然是通过Function
组件。
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组件展示了一个列表。该列表的状态和状态更新是使用useState
。useState
可以动态的管理我们要渲染到组件内的数据。该初始化数据是一个在对象中的空属性列表。现在,该数据为空。
我们将使用axios
来异步获取数据。当然,你可以使用你自己常用的类库或者原生请求api来在浏览器请求数据。如果你至今没有安装axios
,你可以在你项目中执行npm i axios
来安装。接下来你可以这么写。
import React, { useState, useEffect } from 'react';
import axios from '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
方法来调用方法,通过axios
获取数据并赋值本地状态在组件内更新。该promise
请求使用了async/await
。当你运行你的应用之后,你应该遇到一个尴尬的事情。effect
会在组件加载完成和更新的时候都执行。因为我们将该数据请求放在状态更新之后触发,所以该请求进入了死循环。这是一个需要我们避免的bug。我们只想在组件加载完成之后去请求数据。这就是为什么要在effect
的第二个参数放一个空数组的原因,这会避免在状态更新后触发effect
执行,只会在组件加载完成之后执行。
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
所依赖的任意变量。如果其中一个变量变了,就会触发effect
执行。如果这个参数为空数组,则在组件内有变量更新不会执行。这表示其无需监听任何变量的更新。
接下来最后一个问题。代码中使用了async/await
来使用第三方api请求数据。根据文档,用async
前缀的函数都需要返回一个隐式的promise
然而,effect
不需要返回任何东西或者返回一个干净的方法。这就是你在控制台看到如下警告的原因。
这也是为什么不能直接在useEffect
使用async
的原因。我们来解决下,在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
获取数据的例子。如果你对于处理错误处理,加载动画,从form获取数据和处理可复用的获取数据方法感兴趣,你可以继续阅读。
如何以编程/手动方式触发钩子?
nice! 我们已经在页面加载完成获取到数据了。但是如何获取文本框输入的内容呢?默认为‘Redux’查询。但是关于响应式呢?然我们创建一个input元素然后不用'Redux'实现来获取数据。现在,来介绍一种新的为input而设定的状态。
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(
'https://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;
有件事被忽略了,当你在文本框输入内容的时候,在effect
钩子内加载完成之后就不会调接口了。造成这样的原因是你将``effect钩子的第二个参数设置为了空。
effect没有监听任何变量,所以只在页面完全加载完成之后才会触发。然而,现在
effect`的触发取决于查询。一旦文本框输入,请求就会继续。
...
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;
请求应该只在你文本框数值更改才会触发。有另外一个问题:在你每次输入结束,effect
都会触发一个数据请求。如果想要一个按钮来触发呢?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
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>
);
}
现在,使效果取决于搜索状态,而不是随输入字段中的每个键笔划而变化的波动查询状态。一旦用户点击搜索按钮,搜索文字被赋值,然后手动触发effect
执行搜索。
...
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;
作为搜索的值,一开始初始化和state
的值一样。在页面加载完成回去请求数据,将会带上input框的初始值。然后这样操作会有点迷惑。我们是不是可以将参数可以放到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钩子获取隐式编程数据的条件。你可以让effect
来监听你需要的状态变量。在你设置之后,在你点击之后或者绑定监听之后将会触发effect
执行。在本示例中,当url更改将会请求数据。
加载loading效果在React Hooks
我们来介绍下实现加载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(
'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;
当组件加载完成或者url状态更改,会触发effect
钩子加载数据。这时候你只需要将加载组件设置为true
。当数据完全加载将加载组件设置为false
即可。
在React Hooks内处理错误异常
如何在ReactHooks
里处理错误异常呢?错误信息其实就是另一个初始化的状态。一旦出现错误信息,组件就会给到用户反馈。当使用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;
每次钩子函数执行都会重置错误状态。错误请求产生,你可能会想再次尝试。这么处理很OK的。为了测试错误,你可以将url参数设置为错误的,然后验证错误提示是否显示。
从表单内获取数据
来玩玩从form获取数据可好?至今我们只是结合文本框输入和按钮点击来获取数据。一旦你想添加更多的文本框,你就会用forml来处理数据获取。此外,form表单还可以用键盘上的回车来触发数据获取。
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>
);
}
当点击提交按钮之后会触发浏览器刷新,造成刷新的原因是提交表单按钮的触发刷新是浏览器的默认行为。为了避免默认行为,我们可以在React的event中高添加一个方法。如下是class用法的写法。
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>
);
}
现在点击提交按钮不会再触发刷新了。好了,这次我们实现了表单获取数据而不是简单的文本框输入和按钮点就。
自定义数据结构获取数据
为了提取用于数据获取的自定义钩子,请将属于数据获取的所有内容(属于输入字段的查询状态除外,但包括加载指示符和错误处理)移到其自己的函数中。还要确保从应用程序组件中使用的函数中返回所有必需的变量。
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];
}
如今,你可以这么写。
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]);
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;
在hooks
下可以这么写。钩子本身对于api毫无关系。它从外部接收所有参数,只管理必要的状态,如数据、加载和错误状态。它执行请求并将数据返回给组件,将其用作自定义数据获取钩子。
用于数据获取的还原钩子
到目前为止,我们已经使用了各种状态挂钩来管理数据的获取状态、加载状态和错误状态。然而,不知何故,所有这些状态,用它们自己的状态钩子管理,都属于一个整体,因为它们关心相同的原因。如您所见,它们都在数据获取函数中使用。一个很好的迹象表明它们属于一个整体,即它们被一个接一个地使用(例如setIsError、setIsLoading)。让我们用一个异径弯钩来代替这三个。
Reducer钩子返回一个state对象和一个函数来改变state对象。该函数(称为dispatch function)执行一个具有类型和可选负载的操作。所有这些信息都在实际的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钩子将Reducer函数和初始状态对象作为参数。在我们的例子中,数据、加载和错误状态的初始状态的参数没有改变,但是它们被聚合到一个由一个reducer钩子管理的状态对象,而不是单个状态钩子。
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函数发送的对象具有强制类型属性和可选负载属性。类型告诉reducer函数需要应用哪个状态转换,并且reducer还可以使用有效负载来提取新的状态。毕竟,我们只有三种状态转换:初始化获取过程、通知成功的数据获取结果和通知错误的数据获取结果。
在自定义钩子的末尾,状态像以前一样返回,但是因为我们有一个状态对象,而不再是独立状态。这样,调用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\u INIT、FETCH\u SUCCESS和FETCH\u 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函数可以通过其参数访问当前状态和传入操作。到目前为止,in-out switch case语句每个状态转换只返回前一个状态。destructuring语句用于保持state对象不可变(这意味着状态永远不会直接发生变化)以强制实施最佳实践。现在,让我们重写当前状态的几个返回属性,以便在每个状态转换时更改状态:
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();
}
};
现在,由操作的类型决定的每个状态转换都会返回一个基于先前状态和可选负载的新状态。例如,在请求成功的情况下,有效负载用于设置新状态对象的数据。
总之,Reducer钩子确保状态管理的这一部分是用它自己的逻辑封装的。通过提供操作类型和可选的有效负载,您将始终以谓词状态更改结束。此外,您永远不会遇到无效状态。例如,以前可能会意外地将isLoading和isError状态设置为true。对于这种情况,UI中应该显示什么?现在,reducer函数定义的每个状态转换都会导致一个有效的状态对象。
中止数据提取生效
React中的一个常见问题是,即使组件已经卸载,也会设置组件状态(例如,由于使用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]);
return [state, setUrl];
};
每个Effect钩子都有一个clean-up函数,该函数在组件卸载时运行。clean-up函数是从钩子返回的一个函数。在我们的例子中,我们使用一个名为didCancel的布尔标志来让我们的数据获取逻辑知道组件的状态(挂载/卸载)。如果组件确实卸载了,那么标志应该设置为true,这将导致在异步解析数据获取之后无法设置组件状态。
注意:事实上,数据获取不会被中止——这可以通过Axios取消来实现——但是对于未安装的组件,状态转换不再执行。因为Axios取消在我看来并不是最好的API,所以这个防止设置状态的布尔标志也起作用。
end:现在你已经学习了如何在React hooks
中获取数据。如果您对使用呈现道具和高阶组件在类组件(和函数组件)中获取数据感到好奇,请从一开始就查看我的另一篇文章。否则,我希望本文对您学习React钩子以及如何在真实场景中使用它们很有用。
点击源文章查看原文。
翻译:赵辉
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!