【问题标题】:ReactJS useState hook - asynchronous behaviourReactJS useState hook - 异步行为
【发布时间】:2021-08-17 14:56:09
【问题描述】:

我正在建立一个页面来列出产品。 所以我有一个 input:file 按钮来选择多个图像,然后我调用一个 API 将这些图像上传到服务器上,并在 UI 中显示带有图像的进度。 这是我的代码。

import axios from 'axios'
import React, { useState } from 'react'
import { nanoid } from 'nanoid'

const Demo = () => {
    const [images, setImages] = useState([])

    const handleImages = async (e) => {
        let newArr = [...images]
        for (let i = 0; i < e.target.files.length; i++) {
            newArr = [
                ...newArr,
                {
                    src: URL.createObjectURL(e.target.files[i]),
                    img: e.target.files[i],
                    uploaded: 0,
                    completed: false,
                    _id: nanoid(5),
                },
            ]
        }
        setImages(newArr)
        await uploadImages(newArr)
    }

    const uploadProgress = (progress, arr, idx) => {
        let { total, loaded } = progress
        let inPercentage = Math.ceil((loaded * 100) / total)
        let imgs = [...arr]
        imgs[idx]['uploaded'] = inPercentage
        setImages([...imgs])
    }

    const uploadImages = async (imageArr) => {
        for (let i = 0; i < imageArr.length; i++) {
            let formData = new FormData()
            formData.append('img', imageArr[i].img)

            let result = await axios.post(
                `http://localhost:3001/api/demo`,
                formData,
                {
                    onUploadProgress: (progress) =>
                        uploadProgress(progress, imageArr, i),
                }
            )

            if (result?.data) {
                let imgs = [...imageArr]
                imgs[i]['completed'] = true
                setImages([...imgs])
            }
        }
    }

    return (
        <div>
            <input
                type="file"
                multiple
                accept="image/*"
                onChange={handleImages}
            />
            <div>
                <div className="img-container" style={{ display: 'flex' }}>
                    {images.length ? (
                        <>
                            {images.map((img) => (
                                <div style={{ position: 'relative' }}>
                                    <img
                                        style={{
                                            width: '200px',
                                            height: 'auto',
                                            marginRight: '10px',
                                        }}
                                        src={img.src}
                                        alt="alt"
                                    />
                                    {!img.completed && (
                                        <div
                                            style={{
                                                background: 'rgba(0,0,0,0.3)',
                                                padding: '4px',
                                                position: 'absolute',
                                                top: '10px',
                                                left: '10px',
                                                color: 'white',
                                                borderRadius: '5px',
                                            }}
                                        >
                                            {img.uploaded}%
                                        </div>
                                    )}
                                </div>
                            ))}
                        </>
                    ) : null}
                </div>
            </div>
        </div>
    )
}

export default Demo

由于 useState 是异步的,我不能直接将反应状态传递给我的 API 处理程序。 所以现在的问题是,假设我选择了 3 张图片来上传,并且在我的“uploadImages”函数执行完成之前,我尝试选择其他图片来上传,它没有按预期工作,我知道它没有的原因以打算的方式工作。但不知道解决办法。

问题: 假设,用户首先尝试上传 3 张图片。 “uploadImages”的第一个实例将使用参数 newArr 开始执行,该参数将有 3 个图像,我们将其设置为反应状态“图像”。 但是现在当用户尝试在第一个图像完成之前上传其他图像时,另一个“uploadImages”实例将开始执行,现在在 newArr 参数中,它将有一个新添加的图像数组,此方法将尝试设置状态“图像”。

现在我不知道该如何处理。 Preview

【问题讨论】:

    标签: javascript reactjs react-hooks use-state


    【解决方案1】:

    有两个问题。

    1. 每次uploadProgress 运行时,它都会使用uploadImages 函数传递给它的图像数组。换句话说,如果你开始上传图像 A,你会触发一个 uploadProgress 的实例以 imageArr = [A] 运行。如果添加图像 B,则会触发另一个使用 imageArr = [A,B] 运行的单独的 uploadProgress 实例。由于您使用uploadProgress 中的那些单独的imageArrs 设置状态,images 状态从第一个数组交换到第二个数组并返回。 (如果您从uploadProgress 内部登录,您可以看到这一点。)
        const uploadProgress = (progress, arr, idx) => {
            let { total, loaded } = progress
            let inPercentage = Math.ceil((loaded * 100) / total)
            let imgs = [...arr]
            console.log(imgs.length) // will toggle between 1 and 2 
            imgs[idx]['uploaded'] = inPercentage
            setImages([...imgs])
        }
    

    正如其他人所说,您可以使用函数式 setState 模式来解决这个问题。

        const uploadProgress = (progress, idx) => {
            let { total, loaded } = progress
            let inPercentage = Math.ceil((loaded * 100) / total)
            setImages(arr => {
              let imgs = [...arr]
              imgs[idx]['uploaded'] = inPercentage
              return imgs
            })
        }
    
    1. 其次,每次您触发uploadImages 时,它都会开始为每张图片累积上传。这意味着,如果您上传图片 A,稍等片刻,然后添加图片 B,它会再次开始上传图片 A,最终您将上传两个不同的图片 A。如果您随后要添加图片 C,则d 获取图像 A 的三个副本、图像 B 的两个副本和图像 C 的一个副本。您可以通过阻止上传具有进度值的图像或添加一个指示图像上传过程已经开始的新属性来解决此问题。
    const uploadImages = async (imageArr) => {
      for (let i = 0; i < imageArr.length; i++) {
        if (imageArr[i].progress === 0) { // don't upload images that have already started
    
    

    【讨论】:

    • 是的@MynockSpit,这是导致问题的原因,我知道,上传图像方法的 2 个实例同时运行,并尝试使用我在调用 API 时传递的不同 arr 更新 setImages。谢谢你的建议,我试试!
    【解决方案2】:

    您需要使用 setState 回调访问已保存的图像。它将当前图像作为参数传递:

    setImages(prevImages => { // These are you already saved images
            let newArr = [...prevImages]
            for (let i = 0; i < e.target.files.length; i++) {
                newArr = [
                    ...newArr,
                    {
                        src: URL.createObjectURL(e.target.files[i]),
                        img: e.target.files[i],
                        uploaded: 0,
                        completed: false,
                        _id: nanoid(5),
                    },
                ]
            }
            setImages(newArr)
            uploadImages(newArr)
    })
    

    在进度功能中也需要这样做。

    【讨论】:

    • 我已经在 newArr 变量中存储了当前“图像”状态的副本,然后在我的代码的第 7 行附加新添加的图像。但我还是会尝试你的解决方案,谢谢
    • 但是images只是api调用时的图像。如果您在执行 api 调用时更新这些图像,则新图像将保存在 useState 中,但您在第 7 行中引用的图像对象仍然是调用 api 时的先前图像。要访问当前/新保存的图像,您需要使用回调 setImages(prev => ...),prev 是最近保存的图像。
    【解决方案3】:

    我认为问题在于您在更新状态内的图像对象时依赖index。你能试试我的提议吗?这是一个使用您定义的_id 而不是index 的解决方案。

    import axios from 'axios'
    import React, { useState } from 'react'
    import { nanoid } from 'nanoid'
    
    const Demo = () => {
        const [images, setImages] = useState([])
    
        const findImageObj = (id) => images.find(({ _id }) => _id === id)
    
        const handleImages = async (e) => {
            let newArr = [...images]
            for (let i = 0; i < e.target.files.length; i++) {
                newArr = [
                    ...newArr,
                    {
                        src: URL.createObjectURL(e.target.files[i]),
                        img: e.target.files[i],
                        uploaded: 0,
                        completed: false,
                        _id: nanoid(5),
                    },
                ]
            }
            setImages(newArr)
            await uploadImages(newArr)
        }
    
        const uploadProgress = (progress, arr, id) => {
            let { total, loaded } = progress
            let inPercentage = Math.ceil((loaded * 100) / total)
            const img = findImageObj(id)
            if(img) {
                let imgs = [...arr]
                img['uploaded'] = inPercentage
                setImages([...imgs])
            }
        }
    
        const uploadImages = async (imageArr) => {
            for (let i = 0; i < imageArr.length; i++) {
                let formData = new FormData()
                formData.append('img', imageArr[i].img)
    
                let result = await axios.post(
                    `http://localhost:3001/api/demo`,
                    formData,
                    {
                        onUploadProgress: (progress) =>
                            uploadProgress(progress, imageArr, imageArr[i]._id),
                    }
                )
    
                if (result?.data) {
                    const img = findImageObj(imageArr[i]._id)
                    if(img) {
                        let imgs = [...imageArr]
                        img['completed'] = true
                        setImages([...imgs])
                    }
                }
            }
        }
    
        return (
            <div>
                <input
                    type="file"
                    multiple
                    accept="image/*"
                    onChange={handleImages}
                />
                <div>
                    <div className="img-container" style={{ display: 'flex' }}>
                        {images.length ? (
                            <>
                                {images.map((img) => (
                                    <div style={{ position: 'relative' }}>
                                        <img
                                            style={{
                                                width: '200px',
                                                height: 'auto',
                                                marginRight: '10px',
                                            }}
                                            src={img.src}
                                            alt="alt"
                                        />
                                        {!img.completed && (
                                            <div
                                                style={{
                                                    background: 'rgba(0,0,0,0.3)',
                                                    padding: '4px',
                                                    position: 'absolute',
                                                    top: '10px',
                                                    left: '10px',
                                                    color: 'white',
                                                    borderRadius: '5px',
                                                }}
                                            >
                                                {img.uploaded}%
                                            </div>
                                        )}
                                    </div>
                                ))}
                            </>
                        ) : null}
                    </div>
                </div>
            </div>
        )
    }
    
    export default Demo
    

    【讨论】:

      【解决方案4】:

      是的,是异步问题,尝试通过回调访问你的状态:

        const uploadProgress = (progress, idx) => {
              setImages((imagesState) => {
              let { total, loaded } = progress
              let inPercentage = Math.ceil((loaded * 100) / total)
              let imgs = [...imagesState]
              imgs[idx]['uploaded'] = inPercentage
              return [...imgs]
              })
          }
      
      
      const uploadImages = async (imageArr) => {
              for (let i = 0; i < imageArr.length; i++) {
                  let formData = new FormData()
                  formData.append('img', imageArr[i].img)
      
                  let result = await axios.post(
                      `http://localhost:3001/api/demo`,
                      formData,
                      {
                          onUploadProgress: (progress) =>
                              uploadProgress(progress, i),
                      }
                  )
      
                  if (result?.data) {
                      setImages(imagesState => {
                      let imgs = [...imagesState]
                      imgs[i]['completed'] = true
                      return [...imgs]
                      })
                  }
              }
          }
      

      【讨论】:

        【解决方案5】:

        所有回答过这个问题的人都对,谢谢。我应该使用 setState 回调访问状态。所以我发布我更新的代码供其他人参考。 谢谢

        import axios from 'axios'
        import React, { useState } from 'react'
        import { nanoid } from 'nanoid'
        
        const Demo = () => {
            const [images, setImages] = useState([])
        
            const findIdx = (arr, _id) => arr.findIndex((item) => item._id === _id)
        
            const handleImages = async (e) => {
                setImages((prevImgs) => {
                    let newArr = [...prevImgs]
                    for (let i = 0; i < e.target.files.length; i++) {
                        newArr = [
                            ...newArr,
                            {
                                src: URL.createObjectURL(e.target.files[i]),
                                img: e.target.files[i],
                                uploaded: 0,
                                completed: false,
                                _id: nanoid(5),
                            },
                        ]
                    }
                    uploadImages([...newArr])
                    return newArr
                })
            }
        
            const uploadProgress = (progress, _id) => {
                let { total, loaded } = progress
                let inPercentage = Math.ceil((loaded * 100) / total)
                setImages((prevImgs) => {
                    let imgs = [...prevImgs]
                    let idx = findIdx(prevImgs, _id)
                    imgs[idx]['uploaded'] = inPercentage
                    return imgs
                })
            }
        
            const uploadImages = (imageArr) => {
                for (let i = 0; i < imageArr.length; i++) {
                    if (imageArr[i].uploaded !== 0) continue
        
                    let formData = new FormData()
                    formData.append('img', imageArr[i].img)
        
                    axios
                        .post(`http://localhost:3001/api/demo`, formData, {
                            onUploadProgress: (progress) =>
                                uploadProgress(progress, imageArr[i]._id),
                        })
                        .then((result) => {
                            if (result?.data) {
                                setImages((prevImgs) => {
                                    let imgs = [...prevImgs]
                                    let idx = findIdx(prevImgs, imageArr[i]._id)
                                    imgs[idx]['completed'] = true
                                    return imgs
                                })
                            }
                        })
                        .catch((err) => console.log('--> ERROR', err))
                }
            }
        
            return (
                <div>
                    <input
                        type="file"
                        multiple
                        accept="image/*"
                        onChange={handleImages}
                    />
                    <div>
                        <div className="img-container" style={{ display: 'flex' }}>
                            {images.length ? (
                                <>
                                    {images.map((img) => (
                                        <div style={{ position: 'relative' }}>
                                            <img
                                                style={{
                                                    width: '200px',
                                                    height: 'auto',
                                                    marginRight: '10px',
                                                }}
                                                src={img.src}
                                                alt="alt"
                                            />
                                            {!img.completed && (
                                                <div
                                                    style={{
                                                        background: 'rgba(0,0,0,0.3)',
                                                        padding: '4px',
                                                        position: 'absolute',
                                                        top: '10px',
                                                        left: '10px',
                                                        color: 'white',
                                                        borderRadius: '5px',
                                                    }}
                                                >
                                                    {img.uploaded}%
                                                </div>
                                            )}
                                        </div>
                                    ))}
                                </>
                            ) : null}
                        </div>
                    </div>
                </div>
            )
        }
        
        export default Demo
        

        【讨论】:

          猜你喜欢
          • 2021-08-01
          • 1970-01-01
          • 2022-11-15
          • 1970-01-01
          • 2021-07-29
          • 2022-01-08
          • 1970-01-01
          • 1970-01-01
          • 2021-05-20
          相关资源
          最近更新 更多