当您使用React构建一个很棒的应用程序时,通常最终将需要获取远程或异步数据。也许您需要从API中获取一些数据以显示帖子,或获取搜索查询的搜索结果数据。无论使用哪种情况,在React中获取远程数据有时都会有些棘手。
我们将研究定制的React钩子如何在异步获取数据时如何使生活变得更轻松。我们将研究三种在React组件中获取数据的方式。
获取数据我们需要知道什么?
如果您要加载前景数据(即它不在后台,并且对用户很重要),那么我们需要了解几件事。我们想要的最低裸机;
- 加载的数据(如果存在)
- 数据是否正在加载
- 以及加载数据是否出错
为了解决这个问题,我们需要3个不同的状态变量(是的,我知道您可以将它们全部放在一个状态对象中):数据,加载状态和错误,以及根据特定操作正确设置它们的逻辑。
例如,在加载开始时,我们需要将loading设置为true,将error设置为null,然后触发请求。当请求返回时,我们需要将load设置为false,并根据是否成功来设置数据或错误。潜在地,我们可能希望使用“重置”功能将状态重置为默认或空闲状态。
一种获取数据的简单方法
让我们快速回顾一下您可能已经见过或使用过的在React组件中获取数据的方法。这种方法的问题很快就可以解决。
考虑下面的代码示例(或查看下面的代码笔)。
// A sample component to fetch data from an async source
// Note that the 'fetchFn' isn't specified, but assume it
// returns a promise
// this component just shows a list of people,
// its not necessary, just part of the example
const DisplayPeople = ({ people }) => {
return (
<div className="people">
{people.map((person, index) => (
<div className="person" key={index}>
{person.name}
</div>
))}
</div>
);
};
// Here's our component that uses async data
const Component1 = props => {
const [data, setData] = useState();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
const resp = await fetchFn(shouldFail);
setData(resp);
setIsLoading(false);
} catch (e) {
setError(e);
setIsLoading(false);
}
};
return (
<div>
{/\* If not isLoading, show a button to load the data
// otherwise show a loading state \*/ }
{!isLoading ? (
<div>
<button onClick={() => fetchData()}>Load data</button>
</div>
) : (
"Loading..."
)}
{/\* if not isLoading and there is an error state,
display the error \*/ }
{!isLoading && error ? (
<div>
<p>Oh no something went wrong!</p>
</div>
) : null}
{/\* If we have data, show it \*/}
{data ? <DisplayPeople people={data.results} /> : null}
{/\* if there's no data and we're not loading, show a message \*/ }
{!data && !isLoading ? <div>No data yet</div> : null}
</div>
);
};
当单击按钮时,此组件从某个异步源加载数据。
单击该按钮时,需要执行以下操作;
- 将错误状态设置为null(如果以前有错误)
- 将加载状态设置为true(因此我们知道它正在加载)
- 触发数据获取功能并等待响应
- 将响应的加载状态设置为false
- 存储错误或数据响应状态
然后在我们的render函数中,我们要检查一些凌乱的if(是的,我在这里使用了三元运算符,但是您可以使用ifs或switch拥有一个单独的函数)。
那这怎么了?
这没有错。它工作正常,获取数据并显示响应。但是,看看我们需要如何管理三个单独的状态变量?假设您需要在组件中进行两个API调用。或一个呼叫取决于另一个。突然,您至少有6个状态变量(除非您能找到重用它们的方法?)
自定义钩子以获取数据
我们可以稍微更好地解决这些问题。我们可以将完成这项工作所需的逻辑抽象到自定义钩子中。
具体如何执行可能取决于您的应用程序以及使用方式,但是我将向您展示一种相当通用的方法,该方法可用于帮助简化组件。
首先,我们将创建一个自定义钩子,然后将修改该组件以使用它。我将首先向您展示代码(如果您只是在这里进行ol’复制粘贴),然后再讨论它。
定制钩子;我喜欢称他为“ useAsyncData”
import { useState, useEffect } from "react";
//Our custom hook 'useAsyncData'
// Options:
// fetchFn (required): the function to execute to get data
// loadOnMount (opt): load the data on component mount
// clearDataOnLoad (opt): clear old data on new load regardless of success state
const useAsyncData = ({
loadOnMount = false,
clearDataOnLoad = false,
fetchFn = null,
} = {}) => {
// Our data fetching state variables
const [data, setData] = useState();
const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(false);
// A function to handle all the data fetching logic
const loadData = async (event) => {
setIsLoading(true);
setError();
if (clearDataOnLoad === true) setData();
try {
const resp = await fetchFn(event);
setData(resp);
setIsLoading(false);
} catch (e) {
setError(e);
setIsLoading(false);
}
};
// 'onMount'
// maybe load the data if required
useEffect(() => {
if (loadOnMount && fetchFn !== null) loadData();
}, []);
// Return the state and the load function to the component
return { data, isLoading, error, loadData };
};
export default useAsyncData;
和组件,重构为使用自定义钩子
//Component using custom hook
const Component2 = (props) => {
const { data, isLoading, error, loadData } = useAsyncData({
fetchFn: (event) => fetchFn(event),
});
return (
<div>
{!isLoading ? (
<div>
<button onClick={() => loadData()}>Load the data (success)</button>
<button onClick={() => loadData(true)}>Load the data (error)</button>
</div>
) : (
"Loading..."
)}
{!isLoading && error ? (
<div>
<p>Oh no something went wrong!</p>
</div>
) : null}
{data ? <DisplayPeople people={data.results} /> : null}
{!data && !isLoading ? <div>No data yet</div> : null}
</div>
);
};
或者,如果您希望看到它的实际效果,请在此处查看代码笔:
那么这里发生了什么?
我们创建了一个自定义钩子,该钩子接受一个函数(fetchFn)作为参数(它也接受一些其他有用的参数,但这不是必需的)。该函数实际上应该执行数据获取并返回一个承诺,该承诺将与数据一起解析,或者因失败而拒绝。
然后,我们将所有状态变量的内容(与第一个示例几乎完全相同)放入了钩子。
然后,我们创建了一个函数(loadData),该函数可以接受一些任意数据(它将传递给fetcnFn-以防您需要)。然后,loadData会执行我们以前在组件中拥有的所有状态逻辑(setIsLoading,setError等)。loadData还调用fetchFn来实际获取数据。
最后,我们从组件中删除了fetchData函数,而不是设置三个状态变量,我们只使用了钩子;
const { data, isLoading, error, loadData } = useAsyncData({
fetchFn: (event) => fetchFn(event),
});
它使我们的生活更轻松吗?
它有点。这不是完美的。这意味着我们每次需要一些数据时都不必对这三个状态变量进行所有逻辑处理。对于每个API调用,我们仍然必须调用该钩子,但这更好。如果您有一个稍微复杂的数据获取方案,则可以将此自定义钩子组合到另一个自定义钩子中。天空是极限!
专家提示:使用状态机
正如我们友好的邻里状态机爱好者(@davidkpiano)所说;“状态机”。
我不会在这里深入解释状态机,因为它不在范围之内。如果您想在状态机上有一些背景知识,请与David本人和Jason Lengstorf一起尝试该视频,或与CSS技巧有关的文章(特定于 React)。
本质上,(有限)状态机状态机可以具有多个离散(或特定)状态。这可以大大简化我们的逻辑。以上面的例子为例。我们有三个状态变量(不要与我们的机器状态混淆),它们实质上构成了我们的应用程序状态。我们的应用程序可以是空闲的(什么都没有发生),正在加载(我们正在等待数据),成功(我们得到了一些数据)或失败(获取数据时出错)。
使用三个独立的变量,每次我们需要了解应用程序的状态时,我们都必须进行一些if检查(如您在具有所有三元运算符的render方法中所看到的)。
如果改用状态机,则需要检查一件事:状态(例如“空闲”,“正在加载”,“成功”,“错误”)。
状态机的另一个很酷的事情是,我们可以指定该机可以从某些状态转换到哪些状态,以及应该在哪些状态之间运行。本质上是可以预见的。
用于异步数据获取的状态机
我将向您展示如何使用状态机进行异步。数据获取。这很大程度上基于xstate / react文档中的文档,因此请务必进行确认。
对于此示例,我们使用xstate和@ xstate / react,因此您需要将它们安装为依赖项。您可以编写自己的状态机实现并对它做出反应,但是为什么要重新发明轮子呢?这是一个非常好的轮子。
$ yarn add xstate @xstate/react
xstate库提供了状态机的实现,@ xstate / react提供了自定义的react钩子以将其绑定以进行响应。
现在我们需要设置状态机。
// fetchMachine.js
import { Machine } from "xstate";
// The context is where we will store things like
// the state's data (for our API data) or the error
const context = {
data: undefined
};
// This is our state machine
// here we can define our states
// along with what each state should do
// upon receiving a particular action
export const fetchMachine = Machine({
id: "fetch",
initial: "idle",
context,
states: {
idle: {
on: { FETCH: "loading" }
},
loading: {
entry: ["load"],
on: {
RESOLVE: {
target: "success",
actions: (context, event) => {
context.data = { ...event.data };
}
},
REJECT: {
target: "failure",
actions: (context, event) => {
context.error = { ...event.error };
}
}
}
},
success: {
on: {
RESET: {
target: "idle",
actions: \_context => {
\_context = context;
}
}
}
},
failure: {
on: {
RESET: {
target: "idle",
actions: \_context => {
\_context = context;
}
}
}
}
}
});
我们的状态机具有一些上下文或它可以存储的数据,以及一组状态,以及在执行某些操作时应转换为的状态。
例如,我们的初始状态为idle。暂无资料。从状态声明中,我们可以看到,如果它处于空闲状态并接收到FETCH命令,则应过渡到loading。
我们总共有四个状态(空闲,加载,成功,失败),并且我添加了一个“重置”操作,这样我们就可以摆脱数据并在需要时返回空闲状态。
最后,我们需要在组件中从@ xstate / react导入自定义钩子
import { useMachine } from "@xstate/react";
并在我们的组件中使用挂钩。这代替了我们之前的钩子调用。load函数是我们的loadData函数,应将命令“发送”回计算机。
const [state, send] = useMachine(fetchMachine, {
actions: {
load: async (context, event) => {
const { shouldFail = false } = event;
try {
const resp = await fetchFn(shouldFail);
send({ type: "RESOLVE", data: resp });
} catch (e) {
send({ type: "REJECT", error: e });
}
},
},
});
最后,我们需要修改渲染以使用机器状态和上下文。
return (
<div>
{state.value === `idle` ? (
<div>
<button onClick={() => send("FETCH")}>Load the data (success)</button>
<button onClick={() => send("FETCH", { shouldFail: true })}>
Load the data (error)
</button>
</div>
) : null}
{state.value === `loading` ? (
<div>
<p>Loading...</p>
</div>
) : null}
{state.value === `success` ? (
<DisplayPeople people={state.context.data.results} />
) : null}
{state.value === "failure" ? <div>Something went wrong!</div> : null}
{state.value !== "idle" && state.name !== "loading" ? (
<div>
<button onClick={() => send("RESET")}>Reset</button>
</div>
) : null}
</div>
);
如果正确组装(ish),它应该看起来像这样(里程可能会有所不同):
import { useMachine } from "@xstate/react";
import { Machine } from "xstate";
const context = {
data: undefined
};
export const fetchMachine = Machine({
id: "fetch",
initial: "idle",
context,
states: {
idle: {
on: { FETCH: "loading" }
},
loading: {
entry: ["load"],
on: {
RESOLVE: {
target: "success",
actions: (context, event) => {
context.data = { ...event.data };
}
},
REJECT: {
target: "failure",
actions: (context, event) => {
context.error = { ...event.error };
}
}
}
},
success: {
on: {
RESET: {
target: "idle",
actions: \_context => {
\_context = context;
}
}
}
},
failure: {
on: {
RESET: {
target: "idle",
actions: \_context => {
\_context = context;
}
}
}
}
}
});
const Component3 = () => {
const [state, send] = useMachine(fetchMachine, {
actions: {
load: async (context, event) => {
const { shouldFail = false } = event;
try {
const resp = await fetchFn(shouldFail);
send({ type: "RESOLVE", data: resp });
} catch (e) {
send({ type: "REJECT", error: e });
}
},
},
});
return (
<div>
{state.value === `idle` ? (
<div>
<button onClick={() => send("FETCH")}>Load the data (success)</button>
<button onClick={() => send("FETCH", { shouldFail: true })}>
Load the data (error)
</button>
</div>
) : null}
{state.value === `loading` ? (
<div>
<p>Loading...</p>
</div>
) : null}
{state.value === `success` ? (
<DisplayPeople people={state.context.data.results} />
) : null}
{state.value === "failure" ? <div>Something went wrong!</div> : null}
{state.value !== "idle" && state.name !== "loading" ? (
<div>
<button onClick={() => send("RESET")}>Reset</button>
</div>
) : null}
</div>
);
};