本文翻译自ROBIN WIERUCH的文章,详细介绍了使用react hooks请求数据的各种方式以及会涉及到的各种应用。本文稍长,希望对你有帮助。
在本教程中,我想向您展示如在React Hooks中使用useState和useEffet获取数据。我们将使用众所周知的Hacker News API从科技界获取热门文章。您还将实现用于数据获取的自定义hooks,该hooks可在应用程序中的任何位置重用或作为独立节点程序包发布在npm上。
如果您对这个新的React功能一无所知,请查看react相关官方文档。
注意:将来,React Hooks不适用于React中的数据获取。相反,将负责名为Suspense的功能。不过,以下演练是了解React中状态和效果挂钩的好方法。
如果您不熟悉React中的数据获取,请在React article中查看我广泛的数据获取。它带您逐步了解如何使用React类组件获取数据,它如何被打造成可以使用render porps组件和高阶组件达到可重用目的,以及如何处理错误处理和加载微调器。在本文中,我想通过功能组件中的React Hooks向您展示所有这些内容。
import React, { useState } from 'react';
function App() {
const [data, setData] = useState({ hits: [] });
return (
{data.hits.map(item => (
-
{item.title}
))}
);
}
export default App;
App组件显示项目列表(hits = Hacker News文章)。状态和状态更新功能来自称为状态钩子,该状态钩子useState
负责管理要为App组件获取的数据的本地状态。初始状态是代表数据的对象中的匹配的空白列表。现在没有为该数据设置任何状态。
我们将使用axios来获取数据,但是由您决定使用另一个数据获取库或浏览器的本机获取API。如果尚未安装axios,则可以在命令行中使用来安装npm install axios
。然后为数据获取实现effect hooks:
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 (
{data.hits.map(item => (
-
{item.title}
))}
);
}
export default App;
名为useEffect的效果挂钩用于从API用axios提取数据,并使用状态挂钩的update函数将数据设置为组件的本地状态。Promise解析发生在异步/等待中。
但是,在运行应用程序时,您应该陷入一个讨厌的循环。效果挂钩在组件安装时运行,但在组件更新时运行。因为我们在每次获取数据后都设置状态,所以组件会更新,并且效果会再次运行。它一次又一次地获取数据。那是一个错误,需要避免。我们只想在组件安装时获取数据。因此,您可以为效果挂钩提供一个空数组作为第二个参数,以避免在组件更新时激活它,而仅在安装组件时激活它。
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 (
{data.hits.map(item => (
-
{item.title}
))}
);
}
export default App;
第二个参数可用于定义挂钩所依赖的所有变量(在此数组中分配)。如果变量之一更改,则effect hook再次运行。如果带有变量的数组为空,则在更新组件时挂钩根本不会运行,因为它不必监视任何变量。
最后一招。在代码中,我们使用async / await从第三方API获取数据。根据文档,每个带有async注释的函数都返回一个隐含的promise:“ async函数的声明定义了一个异步函数,该函数返回一个AsyncFunction对象。异步函数是通过事件循环异步操作的函数,使用隐式Promise返回其结果。“
但是,效果挂钩应不返回任何内容或清除功能。这就是为什么您可能在开发人员控制台日志中看到以下警告的原因:07:41:22.910 index.js:1452警告:useEffect函数必须返回清除函数,否则将不返回任何内容。不支持Promises和useEffect(async()=> ...),但是您可以在效果内部调用异步函数...
这就是为什么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(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
{data.hits.map(item => (
-
{item.title}
))}
);
}
export default App;
简而言之,这就是使用React钩子获取数据。但是,如果您对错误处理,加载指示符,如何触发从表单中获取数据以及如何实现可重用的数据获取挂钩感兴趣,请继续阅读。
太好了,一旦组件安装,我们就在获取数据。但是,如何使用输入字段告诉API我们感兴趣的主题呢?“ Redux”被用作默认查询。但是关于“effect”的话题呢?让我们实现一个input元素,使某人能够获取“ 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(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
setQuery(event.target.value)}
/>
{data.hits.map(item => (
-
{item.title}
))}
);
}
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;
缺少一点点东西:当您尝试在input中键入内容时,在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('');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
setQuery(event.target.value)}
/>
{data.hits.map(item => (
-
{item.title}
))}
);
}
现在,使效果取决于搜索状态,而不是随输入字段中每个击键而变化的波动查询状态。用户单击按钮后,便会设置新的搜索状态,并应手动触发effect hook。
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 (
setQuery(event.target.value)}
/>
{data.hits.map(item => (
-
{item.title}
))}
);
}
如果是使用effect hook进行隐式编程数据获取的话。您可以决定效果取决于哪个状态。在单击或其他副作用上设置此状态后,该效果将再次运行。在这种情况下,如果URL状态更改,则effect会再次运行以从API提取数据。
让我们为数据获取引入一个加载指示器。这只是由state hook管理的另一个状态。加载标志用于在App组件中呈现加载指示器。
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 (
setQuery(event.target.value)}
/>
{isLoading ? (
Loading ...
) : (
{data.hits.map(item => (
-
{item.title}
))}
)}
);
}
export default App;
一旦调用了effect以进行数据获取(在组件安装或URL状态更改时发生),则加载状态将设置为true。请求解决后,加载状态将再次设置为false。
用React钩子获取数据的错误处理怎么样?该错误只是使用状态挂钩初始化的另一个状态。一旦出现错误状态,App组件即可为用户提供反馈。使用异步/等待时,通常使用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 (
setQuery(event.target.value)}
/>
{isError && Something went wrong ...}
{isLoading ? (
Loading ...
) : (
{data.hits.map(item => (
-
{item.title}
))}
)}
);
}
export default App;
每次挂钩再次运行时,都会重置错误状态。这很有用,因为在失败的请求之后,用户可能想要再次尝试,这将重置错误。为了自己实施错误,您可以将URL更改为无效的内容。然后检查是否显示错误消息。
一个合适的表单来获取数据怎么样?到目前为止,我们只有输入字段和按钮的组合。引入更多输入元素后,您可能需要用form元素包装它们。此外,还可以通过表格使用键盘上的“ Enter”来触发按钮。
function App() {
...
return (
{isError && Something went wrong ...}
...
);
}
但是现在,单击“提交”按钮时,浏览器将重新加载,因为这是浏览器提交表单时的本机行为。为了防止默认行为,我们可以在React事件上调用一个函数。这也是您在React类组件中执行此操作的方式。
function App() {
...
return (
{isError && Something went wrong ...}
...
);
}
现在,单击“提交”按钮后,浏览器不再需要重新加载。它像以前一样工作,但是这次使用的是表单而不是朴素的输入字段和按钮组合。您也可以按键盘上的“ Enter”键。
为了提取用于数据获取的自定义钩子,请将属于数据获取的所有内容(属于输入字段但包含加载指示符和错误处理的查询状态除外)移至其自己的函数。另外,请确保您从App组件中使用的函数返回所有必要的变量。
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];
}
现在,您的新挂钩可以再次在App组件中使用:
function App() {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
return (
...
);
}
初始状态也可以设为通用。将其简单地传递到新的自定义钩子:
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 (
{isError && Something went wrong ...}
{isLoading ? (
Loading ...
) : (
{data.hits.map(item => (
-
{item.title}
))}
)}
);
}
export default App;
使用自定义钩子获取数据即已完成。hook本身对API一无所知。它从外部接收所有参数,并且仅管理必要的状态,例如数据,加载和错误状态。它执行请求,并将数据用作自定义数据获取挂钩,将数据返回给组件。
到目前为止,我们已经使用了各种状态挂钩来管理数据的获取状态、加载状态和错误状态。然而,不知何故,所有这些状态,通过它们自己的状态挂钩管理,都属于一起,因为它们关心同一个原因。如您所见,它们都在数据获取函数中使用。它们属于一起的一个很好的标志是它们被一个接一个地使用(例如setiserror、setisloading)。让我们reducer hook来代替这三个。
reducer钩子返回一个状态对象和一个改变状态对象的函数。这个函数称为dispatch函数,它执行一个具有类型和可选有效负载的操作。所有这些信息都用于实际的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 函数和一个初始状态对象作为参数。在我们的例子中,数据,加载和错误状态的初始状态的参数没有改变,但是它们已经聚合到一个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]);
...
};
现在,在获取数据时,可以使用调度功能将信息发送到reducer功能。使用分派功能发送的对象具有必填type
属性和可选payload
属性。该类型告诉化简器功能需要应用哪个状态转换,并且化简器可以额外使用有效负载来提取新状态。毕竟,我们只有三个状态转换:初始化获取过程,通知成功的数据获取结果,以及通知错误的数据获取结果。
在自定义挂钩的末尾,状态将像以前一样返回,但是因为我们有一个状态对象,而不再是独立状态。这样一来,谁调用了一个useDataApi
自定义的钩子仍然得到访问data
,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 };
case 'FETCH_SUCCESS':
return { ...state };
case 'FETCH_FAILURE':
return { ...state };
default:
throw new Error();
}
};
reducer 函数可以通过其参数访问当前状态和传入操作。到目前为止,在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();
}
};
现在,由操作类型决定的每个状态转换都将基于先前的状态和可选的有效负载返回一个新的状态。例如,在成功请求的情况下,有效负载用于设置新状态对象的数据。
总之,Reducer Hook确保状态管理的这一部分使用其自己的逻辑进行封装。通过提供操作类型和可选的有效负载,您将始终以谓词状态更改为最终结果。此外,您将永远不会陷入无效状态。例如,以前可能会意外将isLoading
and 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];
};
每个效果挂钩都带有清理功能,该功能在卸载组件时运行。清理函数是从挂钩返回的一个函数。在我们的例子中,我们使用一个布尔标志didCancel
来让我们的数据获取逻辑知道组件的状态(已安装/未安装)。如果确实卸载了组件,则应设置该标志true
,以防止在最终异步地解析了数据读取之后设置组件状态。
注意:实际上,数据获取不会中止-这可以通过Axios Cancellation来实现-但是,已卸载的组件不再执行状态转换。由于Axios Cancellation在我眼中并不是最好的API,因此该防止设置状态的布尔值标记也可以完成此工作。
这里放一个官方的获取数据的简单实例:使用react hooks获取数据示例