ReactJS useState 钩子 - 异步行为

ReactJS useState hook - asynchronous behaviour

我正在构建一个页面来列出产品。 所以我有一个 input:file 按钮来 select 多张图片,然后我调用 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 处理程序。 所以现在的问题是,假设我正在 select 上传 3 张图片,并且在我的“uploadImages”函数执行完成之前,我尝试 select 上传其他图片,它不起作用预期而且我知道它没有按预期方式工作的原因。但是不知道解决方法

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

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

您需要使用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)
})

在进度函数中也需要做同样的事情。

我认为问题在于您在更新状态内的图像对象时依赖 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

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

  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]
                })
            }
        }
    }

有两个问题。

  1. 每次 uploadProgress 是 运行,它使用由 uploadImages 函数传递给它的图像数组。换句话说,如果你开始上传图像 A,你会触发 uploadProgress 运行 imageArr = [A] 的实例。如果添加图像 B,则会触发另一个单独的 uploadProgress 运行 imageArr = [A,B] 实例。由于您使用 uploadProgress 中的那些单独的 imageArr 设置状态,因此 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

所有回答过这个问题的人都对,谢谢。我应该使用 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