在通过 React 挂钩中的 useEffects 中的多个异步函数获取数据时,我必须指定唯一的 Loading 布尔值以避免并发问题吗?
Must I specify unique Loading booleans when fetching data via multiple async functions within useEffects in React hooks to avoid concurrency problems?
我担心的是如果使用相同的 isLoading
布尔状态会导致并发问题。这是我正在谈论的示例:
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
async function getWaitlistDetailsOnLoad() {
setIsLoading(true)
try {
const details = await getWaitlistDetailsForUser(
apiInstance,
product.id
);
if (details.status === 200) {
setWaitlistDetails(details.data);
}
} catch (e) {
console.log("Error getting waitlist details: ", e);
}
setIsLoading(false)
}
getWaitlistDetailsOnLoad();
}, [apiInstance, product.id]);
useEffect(() => {
async function getNextAvailableTimeOnLoad() {
setIsLoading(true)
try {
const time = await getNextAvailableTime(apiInstance, product.id);
if (time.status === 200) {
setNextAvailableTime(time.data);
}
setIsLoading(false);
} catch (e) {
console.log("Error getting next available time: ", e);
}
setIsLoading(false)
}
getNextAvailableTimeOnLoad();
}, [apiInstance, product.id]);
当然我可以像这样跟踪两个独立的加载状态:
const [firstLoader, setFirstLoader] = useState<boolean>(false);
const [secondLoader, setSecondLoader] = useState<boolean>(false);
...但如果我不必这样做,我宁愿不这样做。对于我的用例,这将使代码更简单,条件渲染也更简单。
正如@alexander-nied 已经指出的那样,您的代码中可能存在错误。 useEffect 回调不能是异步的。尽管如此,要使用 async/await,您可以按如下方式将三行换行并添加 await 语句。
// Version 1, closer to the original example
useEffect(() => {
async doSomething() {
await someAsyncMethod();
}
(async () => {
setIsLoading(true);
await doSomething();
setIsLoading(false);
})();
})
// Version 2, closer to the current example
useEffect(() => {
async doSomething() {
setIsLoading(true);
await someAsyncMethod();
setIsLoading(false);
}
doSomething();
})
回答你的问题:
这当然取决于你的用例,但我建议不要只使用一个布尔值,因为它允许发生奇怪的场景。
如果一个请求比另一个快得多怎么办?您的加载指示器将设置为 false,而不是通知用户仍有工作在后台完成。
如果你gua运行teed这两个异步效果不会运行同时(例如,如果OperationTwo
,如果在 OperationOne
) 完成时调用一个,那么您可以使用单个 isLoading
布尔值,在 OperationOne
开始时设置为 true
,并且在 OperationTwo
.
完成时设置为 false
但是,如果您有两个可能在任何时候 运行 同时进行的操作,那么您应该将它们分成两个单独的加载器,并使用单个值来 OR
确定视图的最终加载状态。
让我们考虑一个在加载时进行两次异步提取的组件:
const MyPretendComponent = () => {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsLoading(false));
}, [])
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这应该很好地说明了问题——我们有两个关于组件加载的异步操作——一个在一秒内完成,一个在 20 秒内完成。两者都在完成时将 isLoading
设置为 false
。在这种情况下,第一个操作在一秒内完成并将 isLoading
设置为 false
,并且 UI 错误地报告它不处于加载状态,即使第二个操作仍然有 19距离完成还有几秒。
更干净的版本,使用两个布尔值,是这样的:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这里我们将两个加载状态拆分为它们自己的离散布尔值。我们仍然维护一个 isLoading
布尔值,它简单地 OR
两个离散布尔值来确定组件的 总体 加载状态是加载还是未加载。请注意,在现实生活中的示例中,我们希望使用比 isFirstOperationLoading
和 isSecondOperationLoading
.
更具语义描述性的变量名称
关于将两个调用拆分为单独的 useEffect
s: 乍一看,人们可能会认为将两个调用拆分为两个不同的 useEffect
s 你可以减轻或回避异步问题。但是,如果我们遍历组件生命周期,并认为我们会发现这实际上并不有效。让我们更新我们的第一个例子:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这里的问题是组件不是 运行 运行:
- 运行第一个
useEffect
- 当第一个
useEffect
完成时,运行 第二个 useEffect
- 当第二个
useEffect
完成时,渲染 UI
如果它以那种方式 运行,我们将坐等它在加载时显示 UI。
相反,运行s 的方式是这样的:
- 运行 组件渲染——调用任何
useEffect
s(如果适用),然后渲染 UI
- 如果发生任何状态更新(例如,从先前
useEffect
异步调用的 .then
调用状态挂钩)然后再次呈现
- 每次发生状态或道具更新时重复第二步
记住这一点,在每个组件渲染 React 将评估并且运行 任何和所有 适用useEffect
回调。它不是 运行 连续地连接它们并跟踪它们是否已经完成——如果不是不可能的话,这样的事情将是困难的。相反,开发人员有责任跟踪加载状态并组织 useEffect
依赖项数组,以使您的组件状态符合逻辑且一致。
所以你看,用 useEffect
分隔异步调用并不能使我们免于处理潜在的并发异步调用。事实上,如果确实如此,则意味着 UI 加载总体上会变慢,因为您将连续调用两个可以同时调用的调用。例如,如果两个异步调用各花费 10 秒,则一个接一个地调用它们意味着需要 20 秒才能完成加载,而不是 运行 同时调用它们并仅在完成加载之后十秒。从生命周期的角度来看,组件以这种方式运行是有意义的;我们只需要相应地编码。
我担心的是如果使用相同的 isLoading
布尔状态会导致并发问题。这是我正在谈论的示例:
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
async function getWaitlistDetailsOnLoad() {
setIsLoading(true)
try {
const details = await getWaitlistDetailsForUser(
apiInstance,
product.id
);
if (details.status === 200) {
setWaitlistDetails(details.data);
}
} catch (e) {
console.log("Error getting waitlist details: ", e);
}
setIsLoading(false)
}
getWaitlistDetailsOnLoad();
}, [apiInstance, product.id]);
useEffect(() => {
async function getNextAvailableTimeOnLoad() {
setIsLoading(true)
try {
const time = await getNextAvailableTime(apiInstance, product.id);
if (time.status === 200) {
setNextAvailableTime(time.data);
}
setIsLoading(false);
} catch (e) {
console.log("Error getting next available time: ", e);
}
setIsLoading(false)
}
getNextAvailableTimeOnLoad();
}, [apiInstance, product.id]);
当然我可以像这样跟踪两个独立的加载状态:
const [firstLoader, setFirstLoader] = useState<boolean>(false);
const [secondLoader, setSecondLoader] = useState<boolean>(false);
...但如果我不必这样做,我宁愿不这样做。对于我的用例,这将使代码更简单,条件渲染也更简单。
正如@alexander-nied 已经指出的那样,您的代码中可能存在错误。 useEffect 回调不能是异步的。尽管如此,要使用 async/await,您可以按如下方式将三行换行并添加 await 语句。
// Version 1, closer to the original example
useEffect(() => {
async doSomething() {
await someAsyncMethod();
}
(async () => {
setIsLoading(true);
await doSomething();
setIsLoading(false);
})();
})
// Version 2, closer to the current example
useEffect(() => {
async doSomething() {
setIsLoading(true);
await someAsyncMethod();
setIsLoading(false);
}
doSomething();
})
回答你的问题: 这当然取决于你的用例,但我建议不要只使用一个布尔值,因为它允许发生奇怪的场景。
如果一个请求比另一个快得多怎么办?您的加载指示器将设置为 false,而不是通知用户仍有工作在后台完成。
如果你gua运行teed这两个异步效果不会运行同时(例如,如果OperationTwo
,如果在 OperationOne
) 完成时调用一个,那么您可以使用单个 isLoading
布尔值,在 OperationOne
开始时设置为 true
,并且在 OperationTwo
.
false
但是,如果您有两个可能在任何时候 运行 同时进行的操作,那么您应该将它们分成两个单独的加载器,并使用单个值来 OR
确定视图的最终加载状态。
让我们考虑一个在加载时进行两次异步提取的组件:
const MyPretendComponent = () => {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsLoading(false));
}, [])
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这应该很好地说明了问题——我们有两个关于组件加载的异步操作——一个在一秒内完成,一个在 20 秒内完成。两者都在完成时将 isLoading
设置为 false
。在这种情况下,第一个操作在一秒内完成并将 isLoading
设置为 false
,并且 UI 错误地报告它不处于加载状态,即使第二个操作仍然有 19距离完成还有几秒。
更干净的版本,使用两个布尔值,是这样的:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这里我们将两个加载状态拆分为它们自己的离散布尔值。我们仍然维护一个 isLoading
布尔值,它简单地 OR
两个离散布尔值来确定组件的 总体 加载状态是加载还是未加载。请注意,在现实生活中的示例中,我们希望使用比 isFirstOperationLoading
和 isSecondOperationLoading
.
关于将两个调用拆分为单独的 useEffect
s: 乍一看,人们可能会认为将两个调用拆分为两个不同的 useEffect
s 你可以减轻或回避异步问题。但是,如果我们遍历组件生命周期,并认为我们会发现这实际上并不有效。让我们更新我们的第一个例子:
const MyPretendComponent = () => {
const [isFirstOperationLoading, setIsFirstOperationLoading] = useState(false);
const [isSecondOperationLoading, setIsSecondOperationLoading] = useState(false);
useEffect(() => {
setIsFirstOperationLoading(true);
setIsSecondOperationLoading(true)
MyService.fetchThatCompletesInOneSecond()
.then(() => setIsFirstOperationLoading(false));
MyService.fetchThatCompletesInTwentySeconds()
.then(() => setIsSecondOperationLoading(false));
}, [])
const isLoading = isFirstOperationLoading || isSecondOperationLoading;
return (<h1>{`isLoading ? "Loading" : "Finished!"`}</h1>);
}
这里的问题是组件不是 运行 运行:
- 运行第一个
useEffect
- 当第一个
useEffect
完成时,运行 第二个useEffect
- 当第二个
useEffect
完成时,渲染 UI
如果它以那种方式 运行,我们将坐等它在加载时显示 UI。
相反,运行s 的方式是这样的:
- 运行 组件渲染——调用任何
useEffect
s(如果适用),然后渲染 UI - 如果发生任何状态更新(例如,从先前
useEffect
异步调用的.then
调用状态挂钩)然后再次呈现 - 每次发生状态或道具更新时重复第二步
记住这一点,在每个组件渲染 React 将评估并且运行 任何和所有 适用useEffect
回调。它不是 运行 连续地连接它们并跟踪它们是否已经完成——如果不是不可能的话,这样的事情将是困难的。相反,开发人员有责任跟踪加载状态并组织 useEffect
依赖项数组,以使您的组件状态符合逻辑且一致。
所以你看,用 useEffect
分隔异步调用并不能使我们免于处理潜在的并发异步调用。事实上,如果确实如此,则意味着 UI 加载总体上会变慢,因为您将连续调用两个可以同时调用的调用。例如,如果两个异步调用各花费 10 秒,则一个接一个地调用它们意味着需要 20 秒才能完成加载,而不是 运行 同时调用它们并仅在完成加载之后十秒。从生命周期的角度来看,组件以这种方式运行是有意义的;我们只需要相应地编码。