【问题标题】:State changes unexpectedly resets when promise resolves承诺解决时状态更改意外重置
【发布时间】:2021-08-04 04:06:21
【问题描述】:

我有以下代码:

http-common.ts

import axios from 'axios';

export default axios.create({
  baseURL: window.location.origin,
  headers: {
    'Content-type': 'application/json',
  },
});

types.ts

enum UploadStatus {
  NONE,
  UPLOADING,
  DONE,
}

export default UploadStatus;

上传表单.tsx

import * as React from 'react';

import http from './http-common';
import UploadStatus from './types';

type UploadItem = {
  file: File,
  progress: number,
  uploadSuccess: boolean | undefined,
  error: string,
};

const sendUpload = (file: File, onUploadProgress: (progressEvent: any) => void) => {
  const formData = new FormData();
  formData.append('file', file);

  return http.post('/api/uploadFile', formData, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    onUploadProgress,
  });
};

const UploadForm = () => {
  const [fileSelection, setSelection] = useState<UploadItem[]>([]);
  const [currUploadStatus, setUploadStatus] = useState<UploadStatus>(UploadStatus.NONE);

  useEffect(() => {
    if (currUploadStatus === UploadStatus.UPLOADING) {
      const promises: Promise<void>[] = [];

      // Problem code to be discussed

      Promise.allSettled(promises)
        .then(() => setUploadStatus(UploadStatus.DONE));
    }
  }, [currUploadStatus]);

  const handleFileSelectChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files !== null) {
      let newSelection: UploadItem[] = Array.from(event.target.files).map((currFile) => ({
        file: currFile,
        progress: 0,
        uploadSuccess: undefined,
        error: '',
        isChecked: false,
      }));

      if (currUploadStatus === UploadStatus.NONE) {
        newSelection = newSelection.reduce((currSelection, newSelItem) => {
          if (
            currSelection.every((currSelItem) => currSelItem.file.name !== newSelItem.file.name)
          ) {
            currSelection.push(newSelItem);
          }
          return currSelection;
        }, [...fileSelection]);
      }

      setSelection(newSelection);
    }
  };

  // Rest of code
}

我正在尝试将文件从客户端上传到服务器。 sendUpload中的第二个参数onUploadProgress是一个返回当前文件上传进度的函数,让我在屏幕上渲染一个进度条。

在我的第一次尝试中,useEffect 的实现如下:

  useEffect(() => {
    if (currUploadStatus === UploadStatus.UPLOADING) {
      const promises: Promise<void>[] = [];

      fileSelection.forEach((currUploadItem) => {
        const promise: Promise<void> = sendUpload(
          currUploadItem.file,
          (event: any) => {
            setSelection(() => fileSelection.map((currMapItem) => (
              currMapItem.file.name === currUploadItem.file.name
                ? {
                  ...currMapItem,
                  progress: Math.round((100 * event.loaded) / event.total),
                }
                : currMapItem
            )));
          },
        )
          .then(({ data }) => {
            setSelection(() => fileSelection.map((currMapItem) => (
              currMapItem.file.name === currUploadItem.file.name
                ? {
                  ...currMapItem,
                  uploadSuccess: data.uploadSuccess,
                }
                : currMapItem
            )));
          })
          .catch((error) => {
            setSelection(() => fileSelection.map((currMapItem) => (
              currMapItem.file.name === currUploadItem.file.name
                ? {
                  ...currMapItem,
                  error,
                  uploadSuccess: false,
                }
                : currMapItem
            )));
          });

        promises.push(promise);
      });

      Promise.allSettled(promises)
        .then(() => setUploadStatus(UploadStatus.DONE));
    }
  }, [currUploadStatus]);

在上面的useEffect 代码中,我观察到当文件被上传时(即sendUpload(currUploadItem.file, ...) 部分),进度被正确地更新到fileSelection 状态。当我限制浏览器时,我可以看到进度条随着时间的推移正确填满。

但是,一旦 promise 解决并且我到达代码的 .then(...) 部分,进度值会回到 0。在 then(...) 代码段中,uploadSuccess 正确更新为 true,并且此值已成功保存到状态并保持这种状态。换句话说,我可以在屏幕上看到上传成功消息,但我的进度条在达到 100% 后重置为 0%。

在我的第二次尝试中,我将代码更改为如下:

  useEffect(() => {
    if (currUploadStatus === UploadStatus.UPLOADING) {
      const promises: Promise<void>[] = [];

      for (const [idx, item] of fileSelection.entries()) {
        const promise: Promise<void> = sendUpload(
          item.file,
          (event: any) => {
            const updatedSelection = [...fileSelection];
            updatedSelection[idx].progress = Math.round((100 * event.loaded) / event.total);
            setSelection(updatedSelection);
          },
        )
          .then(({ data }) => {
            const updatedSelection = [...fileSelection];
            updatedSelection[idx].uploadSuccess = data.uploadSuccess;
            setSelection(updatedSelection);
          })
          .catch((error) => {
            const updatedSelection = [...fileSelection];
            updatedSelection[idx] = {
              ...updatedSelection[idx],
              error,
              uploadSuccess: false,
            };
            setSelection(updatedSelection);
          });

        promises.push(promise);
      }

      Promise.allSettled(promises)
        .then(() => setUploadStatus(UploadStatus.DONE));
    }
  }, [currUploadStatus]);

在第二个版本中,代码运行良好。 sendUpload(currUploadItem.file, ...) 正确更新我的进度条。当达到 100% 时,上传成功,promise 解析,渲染进度条保持在 100%then(...) 完成,uploadSuccess 更新为 true。我的成功消息出现在屏幕上,并且进度条保持满,这是正确的行为。

为什么第一个版本的代码失败了,而第二个版本成功了?在我看来,这两个版本都在做同样的事情:

  1. 遍历fileSelection 中的每一项。
  2. 对于每个项目,通过axios promise 将文件上传到服务器,并实时获取上传进度
  3. axios更新上传进度时,创建一个新数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,实时更新其进度值。设置状态。
  4. 上传完成后,progress 为 100。promise 现在解析并执行 .then(...)
  5. 创建一个新数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,progress 仍应为 100。将uploadSuccess 更新为从服务器发送的布尔值。设置状态。

我原以为两个版本的代码都在做上述相同的事情。然而由于某种原因,第一个版本在第 4 步保存progress100 到状态,但在第5 步又回到0。第二个版本在第4 步保存progress100 到状态,但在第5 步它保持在100应该的。

发生了什么?

【问题讨论】:

  • 在此处粘贴您的代码 jshint.com 在顶部添加 /* jshint esversion: 6 */ 看看我看到了什么。
  • 只是好奇,为什么将currUploadStatus 状态传递给useEffect 依赖数组?我的意思是这不应该有点像这样的行为:useEffect() - &gt; .then().setState() - &gt; rerender &amp; update currUploadStatus which triggers useEffect() again fails if statement and rerenders the component again 还是我错过了什么?
  • @zergski currUploadStatus 是一个枚举类型,具有三个可能的值:none、uploading 或 done。无是初始值。提交表单后,它会更改为 Uploading。 useEffect 中的代码应仅针对此更改执行。当所有的 Promise 都完成后,它会变成 Done。如果用户选择了一组新的文件,屏幕上的一些状态文本会变回无。
  • 是的,但是因为 currUploadStatus 被传递给依赖数组。当 promises 完成并变为 DONE 时,它不会再次触发 useEffect 钩子,重新渲染组件吗?
  • @zergski 是的,useEffect 会再次触发,但是有一个if (currUploadStatus === UploadStatus.UPLOADING) 所以内部代码只有在currUploadStatus 更改为专门上传时才会运行。 useEffect 检测到所有其他更改,但不执行任何操作。

标签: javascript reactjs promise axios


【解决方案1】:

您的第二个代码复制了数组,但它仍然包含相同的对象,其中一个对象随后在行中变异

updatedSelection[idx].progress = Math.round((100 * event.loaded) / event.total);

您的第一个代码也通过使用扩展语法正确地克隆了对象。但是,问题在于,在每个事件中,它一次又一次地将效果执行时的旧 fileSelection 值作为其起点。请注意,如果您一次上传多个文件,情况会更糟。

原因是关闭了陈旧的constant,如React - useState - why setTimeout function does not have latest state value?How To Solve The React Hook Closure Issue?How to deal with stale state values inside of a useEffect closure?this blog article 中所述。

要解决此问题,请使用setSelection 的回调版本:

  useEffect(() => {
    if (currUploadStatus === UploadStatus.UPLOADING) {
      const promises = fileSelection.map(uploadItem =>
        sendUpload(
          uploadItem.file,
          (event: any) => {
            setSelection(currSelection =>
//                       ^^^^^^^^^^^^^
              currSelection.map(item => item.file.name === uploadItem.file.name
                ? {
                  ...item,
                  progress: Math.round((100 * event.loaded) / event.total),
                }
                : item
              )
            );
          },
        )
          .then(({data}) => ({
            uploadSuccess: data.uploadSuccess
          }, error => ({
            error,
            uploadSuccess: false
          })
          .then(result => {
            setSelection(currSelection =>
//                       ^^^^^^^^^^^^^
              currSelection.map(item => item.file.name === uploadItem.file.name
                ? {
                  ...item,
                  ...result,
                }
                : item
              )
            );
          })
      );

      Promise.all(promises)
        .then(() => setUploadStatus(UploadStatus.DONE));
    }
  }, [currUploadStatus]);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-11
    • 1970-01-01
    • 2021-09-12
    • 1970-01-01
    • 2021-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多