简体   繁体   English

ReactJS useState 挂钩 - 异步行为

[英]ReactJS useState hook - asynchronous behaviour

I am building a page to list products.我正在建立一个页面来列出产品。 So I've one input:file button to select multiple images and then I'm calling an API to upload that images on the server and displaying progress in UI with images.所以我有一个 input:file 按钮到 select 多个图像,然后我调用 API 将这些图像上传到服务器上并在 UI 中显示带有图像的进度。 Here's my code.这是我的代码。

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

As the useState is asynchronous, I can't directly pass that react state to my API handler.由于 useState 是异步的,我不能直接将反应 state 传递给我的 API 处理程序。 So now the issue is, suppose I'm selecting 3 images to upload, and before the execution of my "uploadImages" function completes, I try to select other images to upload, It's not working as expected and I know the reason why it's not working the way intend to.所以现在的问题是,假设我选择上传 3 张图片,并且在我的“uploadImages”function 执行完成之前,我尝试 select 其他图片上传,它没有按预期工作,我知道它不正常的原因以打算的方式工作。 But don't know the solution.但不知道解决办法。

Issue: suppose, the user first tries to upload 3 images.问题:假设用户首先尝试上传 3 张图片。 the first instance of "uploadImages" will begin its execution with parameter newArr which would have 3 images and we're setting it to react-state "images". “uploadImages”的第一个实例将使用参数 newArr 开始执行,该参数将有 3 张图像,我们将其设置为反应状态“图像”。 But now when the user tries to upload other images before letting the first ones finish, another instance of "uploadImages" will begin its execution and now in newArr parameter, it would have an array of newly added images and this method will try to set state "images".但是现在当用户在第一个图像完成之前尝试上传其他图像时,“uploadImages”的另一个实例将开始执行,现在在 newArr 参数中,它将有一个新添加的图像数组,此方法将尝试设置 state “图片”。

Now I don't know how can I handle this.现在我不知道我该如何处理。 Preview预习

You need to access the already saved images with the setState callback.您需要使用 setState 回调访问已保存的图像。 Which will pass the current images as a parameter:它将当前图像作为参数传递:

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

The same needs to be done in the progress function.在进度function中也需要做同样的事情。

I think the problem is that you rely on the index when updating the images objects inside the state.我认为问题在于您在更新 state 中的图像对象时依赖index Can you try my proposal?你能试试我的提议吗? It is a solution that uses the _id you have defined instead of the 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

Yes is a asynchronous problem, try to access to your state via callback:是的,是异步问题,尝试通过回调访问您的 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]
                })
            }
        }
    }

There are two problems.有两个问题。

  1. Each time uploadProgress is run, it's using the images array that was passed to it by the uploadImages function.每次运行uploadProgress时,它都会使用uploadImages function 传递给它的图像数组。 In other words, if you start uploading image A, you trigger an instance of uploadProgress running with imageArr = [A] .换句话说,如果你开始上传图片 A,你会触发一个使用imageArr = [A]运行的uploadProgress实例。 If you add image B, you trigger another separate instance of uploadProgress running with imageArr = [A,B] .如果添加图像 B,则会触发另一个使用imageArr = [A,B]运行的单独的 uploadProgress 实例。 Since you're setting state using those separate imageArr s in uploadProgress , the images state swaps from being the first array to the second array and back.由于您使用 uploadProgress 中的那些单独的imageArr设置uploadProgress ,因此images state 从第一个数组交换到第二个数组并返回。 (You can see this if you log from inside uploadProgress .) (如果您从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])
    }

As others have said, you can solve this by using the functional setState pattern.正如其他人所说,您可以通过使用功能 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. Second, each time you trigger uploadImages it starts the upload for every image, cumulatively.其次,每次触发uploadImages时,它都会累积地开始为每个图像上传。 That means if you upload image A, wait a second, then add image B, it'll start uploading image A again and you'll end up with two different upload of image A. If you then were to add image C, you'd get three copies of image A, two copies of image B, and one of image C.这意味着如果您上传图片 A,稍等片刻,然后添加图片 B,它将再次开始上传图片 A,您最终会上传两个不同的图片 A。如果您随后要添加图片 C,您d 得到图像 A 的三个副本、图像 B 的两个副本和图像 C 的一个副本。 You can solve this by preventing upload of images that have progress value or add a new property that indicates the image upload process has been started already.您可以通过阻止上传具有进度值的图像或添加指示图像上传过程已开始的新属性来解决此问题。
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

All those who've answered this question, all are right, Thanks.所有回答过这个问题的人都对,谢谢。 I should access the state using setState callback.我应该使用 setState 回调访问 state。 So I'm posting my updated code for other's reference.所以我发布我更新的代码供其他人参考。 Thanks谢谢

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