我有一个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块中成功地将我的文件上传到Edsterore存储桶并将正确接收的res.url分配给urls状态时,当函数到达.then块时,状态还没有准备好,urls仍然是一个空数组,在数据库中插入了一条不完整的记录.

如何管理仅在状态准备就绪时才try 在数据库中创建记录?

我试着try 使用依赖数组中有urlsuseEffect,但这使得useEffect触发的次数与我上传的文件一样多(因为Promise.all).

我怎么才能解决这个问题呢?

推荐答案

你面临的问题是批量处理.

什么是配料?

React.js自版本18以来提供了一个名为批处理的新概念.这意味着它将某些状态更新推迟到下一次重新呈现以避免不必要的重新呈现.

在事件处理程序中自动react 批处理状态(如您的 case )和setTimeout回调.简而言之,当您在事件处理程序(onClickonSubmit等)中设置状态时,在事件处理程序结束之前,Reaction不会重新呈现(更新状态).

示例

const [state, setState] = useState(1);

const onSubmit = async (values: ValuesType) => {
  await doSomething(values);

  // Currently, state === 1
  
  setState(2)
  
  // Still, state === 1, not 2
}

阅读有关Reaction中批处理的更多信息:

你的例子怎么样?

   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重新呈现组件,它将销毁整个事件处理程序.设想每次重新呈现都会顺利地上载第一个文件,然后当它到达前一行时,Reaction重新呈现组件,并从头开始执行事件处理程序,直到它到达同一行,然后重新呈现,依此类推……这将是一个无限循环.

相反,它会做什么呢?

让我们简化一下您的函数

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.
}

在此事件处理程序完成删除后,Reaction将在下一次重新呈现中运行此操作

let numbers = [];

const batchesUpdates = [
  function (nums) { return [...nums, 1] },
  function (nums) { return [...nums, 2] },
]

batchesUpdates.forEatch(func => {
  numbers = func(numbers)
})

明白了?

如何处理您的用例?

  1. 可以在事件处理程序中声明内部变量,并使用它
 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");
    }
  };

高级: Select 加入/退出批处理

要 Select 退出自动批处理,您可以使用flushSync

import { flushSync } from "react-dom";

const onSubmit = (event) => {
  doSomeLogic()

  flushSync(() => { setProgressBar(w => w++) })

  doSomeOtherLogic()
}

为既不是事件处理程序也不是超时回调的函数手动批处理订阅unstable_batchedUpdates

import { unstable_batchedUpdates as batchedUpdates } from "react-dom";

const doLogic = batchedUpdates(() => { /* batched */ })

Reactjs相关问答推荐

LocalStore未存储正确的数据

无法使用Apollo客户端GraphQL设置Next.js 14

根据用户 Select 的注册类型更改表单字段

滚动视图样式高度没有任何区别

React 错误: 只能用作 元素的子元素,从不直接渲染.请将您的 包裹在

onClick 事件处理程序未应用于样式组件

生产部署:控制台中 Firebase 无效 api 密钥错误

React-dom 的钩子调用无效.我能做些什么?

ReactJS 中的图像加载顺序

React Router v6 路径中的符号

React 组件列表中的第一个计时器正在获取值 NaN

带有 webpack 错误多个文件匹配路由名称的 Expo-router.无法Bundle 或部署应用程序

MUI - ButtonGroup fullWidth 条件屏幕大小不起作用?

使用 axios 响应路由 Dom 操作

null 不是对象(判断RNSound.IsAndroid)

使用 react-router-dom 时弹出空白页面

Storybook - 无法解析generated-stories-entry.cjs/字段browser不包含有效的别名配置

当父组件重新渲染时做出react ,我失go 了子状态.我错过了什么?

CORS 政策:无访问控制允许来源-AWS 和 Vercel

React 似乎在触发另一个组件时重新渲染了错误的组件