上一篇文章我们简述了React Hook 的简单数据获取,包括如何处理错误、如何加载指示符、如何触发从表单获取数据。这是上一篇文章的进阶,将主要讲述如何实现可重用的数据获取 Hook。我希望本文对您进一步了解 React Hook 以及如何在现实场景中使用它们有所帮助。
自定义数据获取的Hook
为了提取用于数据获取的自定义 Hook,将属于数据获取的所有内容(属于输入字段的查询状态除外,但包括加载指示器和错误处理)移动到自己的函数中。还要确保从 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];
}
现在,你的新 Hook 可以再次在 App
组件中使用:
unction 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:
const useDataApi = (initialUrl, initialData) => {
const [data, setData] = useState(initialData);
const [url, setUrl] = useState(initialUrl);
...
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: [] },
);
...
</Fragment>
);
}
export default App;
这就是使用自定义 Hoo k获取数据的方法。Hook 本身并不知道 API 的任何信息。它从外部接收所有参数,只管理必要的状态,如 data
、isLoading
和 isError
。它执行请求并将数据返回给使用它作为自定义数据获取 Hook 组件。
用于数据获取的 Reducer Hook
到目前为止,我们已经使用了各种状态 Hook 来管理数据的数据获取、加载和错误状态。然而,所有这些状态 与管理自己的状态Hook组合在一起,因为他们关心相同的原因。如您所见,它们都在数据获取函数中使用。它们逐个地被使用(例如 setIsError
、setIsLoading
)很好的表明它们属于一起的。让我们使用 Reducer Hook 将这三个 Hook 组合在一起。
Reducer Hook返回一个状态对象和一个改变状态对象的函数。称为调度函数的函数接受一个操作,该操作具有类型和可选的有效负载。所有这些信息都用于实际的 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 而不是单个状态 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 函数。与 dispatch
函数一起发送的对象有一个必选的类型属性和一个可选的有效负载属性。类型告诉 Reducer 功能需要应用哪种状态转换,Reducer 还可以使用负载来提取新状态。毕竟,我们只有三种状态转换:初始化获取过程、通知成功的数据获取结果和通知错误的数据获取结果。
像以前一样,自定义 Hook 最后返回状态,但是因为我们有一个状态对象而不再是独立的状态。这样,调用 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,
});
...
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
和 isError
状态设置为 true
。在这种情况下,UI中应该显示什么呢?现在,reducer 函数定义的每个状态转换都指向一个有效的状态对象。
中止有效Hook中的数据获取
这是一个常见的问题,在React组件的状态设置,即使组件已经卸载(例如,由于导航离开React路由器)。我以前在这里写过关于这个问题的文章,描述了如何防止在各种场景中为卸载的组件设置状态。让我们看看如何防止在我们的自定义Hook中设置状态来获取数据:
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
的布尔标志来让我们的数据获取逻辑知道组件的状态(已挂载/未挂载)。如果组件确实卸载了,该标志应该设置为 true
,这将防止在最终异步解析数据获取之后设置组件状态。
注意:实际上数据获取并没有被中止 —— 这可以通过 Axios Cancellation 实现 —— 但是对于卸载的组件不再执行状态转换。因为 Axios Cancellation
在我看来并不是最好的 API,所以使用这个布尔标记可以防止设置状态。
您已经了解了如何在React中使用用于状态和效果的 React Hook 来获取数据。如果您对带有 render props 和高阶组件的类组件(和函数组件)中的数据获取感到好奇,请参阅我的其他文章。
评论 (0)