繁体   English   中英

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 是异步的,我不能直接将反应 state 传递给我的 API 处理程序。 所以现在的问题是,假设我选择上传 3 张图片,并且在我的“uploadImages”function 执行完成之前,我尝试 select 其他图片上传,它没有按预期工作,我知道它不正常的原因以打算的方式工作。 但不知道解决办法。

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

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

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

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

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

是的,是异步问题,尝试通过回调访问您的 state:

  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 function 传递给它的图像数组。 换句话说,如果你开始上传图片 A,你会触发一个使用imageArr = [A]运行的uploadProgress实例。 如果添加图像 B,则会触发另一个使用imageArr = [A,B]运行的单独的 uploadProgress 实例。 由于您使用 uploadProgress 中的那些单独的imageArr设置uploadProgress ,因此images state 从第一个数组交换到第二个数组并返回。 (如果您从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 回调访问 state。 所以我发布我更新的代码供其他人参考。 谢谢

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM