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]
})
}
}
}
有两个问题。
- 每次
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
})
}
- 其次,每次触发
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
我正在构建一个页面来列出产品。 所以我有一个 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]
})
}
}
}
有两个问题。
- 每次
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
})
}
- 其次,每次触发
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