我有一个
onSubmit
函数,大致如下所示:
const [isPending, startTransition] = useTransition();
const [urls, setUrls] = useState<string[]>([]);
const { edgestore } = useEdgeStore();
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await Promise.all(
filesToAdd.map(async (addedFileState) => {
const res = await edgestore.publicFiles.upload({
file: addedFileState.file as File,
});
setUrls((prev) => [...prev, res.url]);
}),
)
.then(() => {
startTransition(() => {
createRecord({ imagesUrls: urls, name: values.name })
});
})
} catch {
toast.error("Something went wrong");
}
};
我遇到的问题是,在
try
块中,我成功地将文件上传到 Edgestore 存储桶,并将正确接收的 res.url
分配给 urls
状态,当函数命中 .then
块时state 尚未准备好,urls
仍然是一个空数组,在数据库中插入一条不完整的记录。
如何设法仅在状态就绪后才尝试在数据库中创建记录?
我尝试查看是否可以利用依赖项数组中具有
useEffect
的 urls
,但这使得 useEffect 触发的次数与我上传的文件一样多(因为 Promise.all
)。
我该如何解决这个问题?
您面临的问题是批处理。
React.js 从版本 18 开始提供了一个新概念,称为批处理。这意味着它将一些状态更新推迟到下一次重新渲染,以避免不必要的重新渲染。
在事件处理程序(如您的情况)和
setTimeout
回调中自动反应批处理状态。简而言之,当您在事件处理程序中设置状态时(onClick
、onSubmit
等),React 不会重新渲染(更新状态),直到事件处理程序结束。
const [state, setState] = useState(1);
const onSubmit = async (values: ValuesType) => {
await doSomething(values);
// Currently, state === 1
setState(2)
// Still, state === 1, not 2
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await Promise.all(
filesToAdd.map(async (addedFileState) => {
const res = await edgestore.publicFiles.upload({
file: addedFileState.file as File,
});
setUrls((prev) => [...prev, res.url]);
}),
)
.then(() => {
startTransition(() => {
createRecord({ imagesUrls: urls, name: values.name })
});
})
} catch {
toast.error("Something went wrong");
}
};
下一行位于事件处理程序和 for 循环中:
setUrls((prev) => [...prev, res.url]);
如果 React 重新渲染组件,它将销毁整个事件处理程序。想象一下,每次重新渲染都会顺利上传第一个文件,然后当它到达上一行时,React 重新渲染组件并从头开始执行事件处理程序,直到到达同一行然后重新渲染,如此等等。 ..这将是一个无限循环。
它的作用是什么?
让我们简化您的功能
const [numbers, setNumbers] = useState<number[]>([])
const onSubmit = () => {
// here, numbers = []
setNumbers(nums => [...nums, 1])
// At the moment, numbers still equal = []
// But React remembers that when it finish the event handler
// It will run this function `nums => [...nums, 1]` on the numbers state
setNumbers(nums => [...nums, 2])
// What do you think `numbers` equal?
// Yes, numbers = [], and react will re-rember to run this function
// nums => [...nums, 2]
console.log(numbers);
// What is the output?
// empty array.
}
此事件处理程序完成执行后,React 将在下一次重新渲染时运行此事件
let numbers = [];
const batchesUpdates = [
function (nums) { return [...nums, 1] },
function (nums) { return [...nums, 2] },
]
batchesUpdates.forEatch(func => {
numbers = func(numbers)
})
明白了吗?
const onSubmit = async (values: z.infer<typeof formSchema>) => {
// internal variable
const updloadedFilesUrls = [];
try {
await Promise.all(
filesToAdd.map(async (addedFileState) => {
const res = await edgestore.publicFiles.upload({
file: addedFileState.file as File,
});
// update the internal variable
updloadedFilesUrls.push(res.url)
}),
)
.then(() => {
startTransition(() => {
// use the internal variable
createRecord({ imagesUrls: updloadedFilesUrls, name: values.name })
});
// Update the state
setUrls(urls => [...urls, ...updloadedFilesUrls])
})
} catch {
toast.error("Something went wrong");
}
};