React useState 多次触发
React useState fires multiple times
我有一个点赞按钮,点击后默认值 setLikedNumbers(likedNumbers + 1);
增加 1。再次按下按钮时,使用 setLikedNumbers(likedNumbers - 1);
减一 这工作正常,直到每秒多次按下按钮,这会产生一些奇怪的值。 React 严格模式标签已删除。
问题视频:https://vimeo.com/593482477
一开始我慢慢地点击按钮,然后我每秒进行多次,然后 axios 赶上了请求。
即使在 axios return 错误代码和 return 当前值 - 1 之前按钮被递增多次,这不应该意味着原始值被保留为数字增加的数量等于减少的数量?
我怀疑问题所在的代码(我删除了一些不需要的行):
组件
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
javascript
const like = async (id) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/like`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
});
};
const unlike = async (id) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/unlike`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
};
整个代码
import React from "react";
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
import Box from "@material-ui/core/Box";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import Typography from "@material-ui/core/Typography";
import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
import BookmarkBorderIcon from "@material-ui/icons/BookmarkBorder";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import { Carousel } from "react-responsive-carousel";
import ReportOutlinedIcon from "@material-ui/icons/ReportOutlined";
import ShareOutlinedIcon from "@material-ui/icons/ShareOutlined";
import FavoriteOutlinedIcon from "@material-ui/icons/FavoriteOutlined";
import BookmarkOutlinedIcon from "@material-ui/icons/BookmarkOutlined";
import ToggleIcon from "material-ui-toggle-icon";
const axios = require("axios");
const CardElement = (props) => {
const [open, setOpen] = React.useState(false);
const [liked, setLiked] = React.useState(props.liked);
const [saved, setSaved] = React.useState(props.saved);
const [likedNumbers, setLikedNumbers] = React.useState(props.numbersLiked);
const handleClickOpen = () => {
setOpen(true);
};
const like = async (id) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/like`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
});
};
const unlike = async (id) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/unlike`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
};
const save = (id) => {
setSaved(true);
axios
.request({
method: "POST",
url: `http://localhost:5000/save`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch((err) => {
setSaved(false);
});
};
return (
<div>
<Card className="card">
<CardActionArea
onClick={() => {
handleClickOpen();
}}
>
{props.mainImg ? (
<CardMedia
className="mediaImgOverview"
image={"http://localhost:5000/image/" + props.mainImg}
/>
) : (
""
)}
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{props.title}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{props.description.length > 45
? props.description.substring(0, 45) + "..."
: props.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions className="ButtonHolder">
<Box className="likesContainer">
{props.likeButtonVisible ? (
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
) : (
""
)}
<Typography
style={{ marginLeft: props.likeButtonVisible ? 0 : "0.2vmax" }}
>
{likedNumbers}
</Typography>
</Box>
{props.likeButtonVisible ? (
<ToggleIcon
on={saved}
onIcon={
<BookmarkOutlinedIcon onClick={() => unsave(props.idData)} />
}
offIcon={
<BookmarkBorderIcon onClick={() => save(props.idData)} />
}
/>
) : (
""
)}
</CardActions>
</Card>
<Dialog
maxWidth="md"
onClose={handleClose}
aria-labelledby="MoreInfo"
open={open}
>
<DialogTitle id="MoreInfo" onClose={handleClose}>
{props.title}
</DialogTitle>
<DialogContent dividers>
{props.images[0].url ? (
<Carousel infiniteLoop="true">
{props.images.map((el) => {
return (
<div key={Math.random()}>
<img alt="" src={"http://localhost:5000/image/" + el.url} />
<p className="legend">{el.caption}</p>
</div>
);
})}
</Carousel>
) : (
""
)}
<Typography gutterBottom>
<Typography>
<b>Категория: </b>
{category(props.category)}
<b> Опасно: </b>
{dangerous(props.dangerous)}
<b> Цена: </b>
{price(props.price)} <b> Достъпност: </b>
{accessibility(props.accessibility)}
</Typography>
{props.description}
</Typography>
</DialogContent>
<DialogActions className="btnCard">
<Box className="likesContainer">
{props.likeButtonVisible ? (
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
) : (
""
)}
<Typography
style={{ marginLeft: props.likeButtonVisible ? 0 : "0.2vmax" }}
>
{likedNumbers == 0
? "Няма харесвания"
: likedNumbers == 1
? "1 харесване"
: likedNumbers + " харесвания"}
</Typography>
</Box>
<Box>
{props.likeButtonVisible ? (
<ToggleIcon
on={saved}
onIcon={
<BookmarkOutlinedIcon onClick={() => unsave(props.idData)} />
}
offIcon={
<BookmarkBorderIcon onClick={() => save(props.idData)} />
}
/>
) : (
""
)}
<ShareOutlinedIcon />
{props.reportButtonVisible ? <ReportOutlinedIcon /> : ""}
</Box>
</DialogActions>
</Dialog>
</div>
);
};
export default CardElement;
更新 1
经过几次实验,我相当确定问题出在按钮(动画)的缓慢变化导致触发其他功能,例如喜欢而不是不同,反之亦然。我正在尝试找到解决这个问题的方法。
更新 2
通过创建一个包装函数来解决问题,该包装函数调用正确的 like/unlike 函数,而不管存在哪个按钮。
wrapper函数调用正确的like unlike函数解决了动画引起的问题
我建议查看 RxJS lib 并限制对 api 的请求数量(这将提高浏览器性能)。
查看下面的简短片段:
// Create a subject which will store the latest value
const buttonClicked = new Subject<{itemId: string, isLiked: boolean}>();
// Debounce for 200 sec.
// debounceTime - Emits a notification from the source Observable only after a particular time span has passed without another source emission. (ref: https://rxjs.dev/api/operators/debounceTime)
const buttonClickedDebounced = buttonClicked.pipe(debounceTime(200));
// If no click was triggered due the time, execute the call to api with latest params.
buttonClickedDebounced.subscribe(({itemId: string, isLiked: boolean}) =>
{
axios.request({...})
// After you get a response it's nice to update the number or likes as well, as soon the other User could like/unlike the same card.
}
);
// Register the click event
function like(itemId, isLiked) {
setLiked(isLiked)
buttonClicked.next({itemId, isLiked})
}
使用这种方法,您将仅向 api 发送 1 个请求,其中包含用户在多次点击后决定离开(喜欢或不喜欢)的最新状态。
希望这有助于改进您的申请!
我有一个点赞按钮,点击后默认值 setLikedNumbers(likedNumbers + 1);
增加 1。再次按下按钮时,使用 setLikedNumbers(likedNumbers - 1);
减一 这工作正常,直到每秒多次按下按钮,这会产生一些奇怪的值。 React 严格模式标签已删除。
问题视频:https://vimeo.com/593482477 一开始我慢慢地点击按钮,然后我每秒进行多次,然后 axios 赶上了请求。
即使在 axios return 错误代码和 return 当前值 - 1 之前按钮被递增多次,这不应该意味着原始值被保留为数字增加的数量等于减少的数量?
我怀疑问题所在的代码(我删除了一些不需要的行):
组件
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
javascript
const like = async (id) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/like`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
});
};
const unlike = async (id) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/unlike`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
};
整个代码
import React from "react";
import "react-responsive-carousel/lib/styles/carousel.min.css"; // requires a loader
import Box from "@material-ui/core/Box";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import Typography from "@material-ui/core/Typography";
import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
import BookmarkBorderIcon from "@material-ui/icons/BookmarkBorder";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import { Carousel } from "react-responsive-carousel";
import ReportOutlinedIcon from "@material-ui/icons/ReportOutlined";
import ShareOutlinedIcon from "@material-ui/icons/ShareOutlined";
import FavoriteOutlinedIcon from "@material-ui/icons/FavoriteOutlined";
import BookmarkOutlinedIcon from "@material-ui/icons/BookmarkOutlined";
import ToggleIcon from "material-ui-toggle-icon";
const axios = require("axios");
const CardElement = (props) => {
const [open, setOpen] = React.useState(false);
const [liked, setLiked] = React.useState(props.liked);
const [saved, setSaved] = React.useState(props.saved);
const [likedNumbers, setLikedNumbers] = React.useState(props.numbersLiked);
const handleClickOpen = () => {
setOpen(true);
};
const like = async (id) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/like`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
});
};
const unlike = async (id) => {
await setLiked(false);
await setLikedNumbers(likedNumbers - 1);
axios
.request({
method: "POST",
url: `http://localhost:5000/unlike`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch(async (err) => {
await setLiked(true);
await setLikedNumbers(likedNumbers + 1);
};
const save = (id) => {
setSaved(true);
axios
.request({
method: "POST",
url: `http://localhost:5000/save`,
headers: {
jwt: localStorage.getItem("jwt"),
},
data: {
place_id: id,
},
})
.catch((err) => {
setSaved(false);
});
};
return (
<div>
<Card className="card">
<CardActionArea
onClick={() => {
handleClickOpen();
}}
>
{props.mainImg ? (
<CardMedia
className="mediaImgOverview"
image={"http://localhost:5000/image/" + props.mainImg}
/>
) : (
""
)}
<CardContent>
<Typography gutterBottom variant="h5" component="h2">
{props.title}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{props.description.length > 45
? props.description.substring(0, 45) + "..."
: props.description}
</Typography>
</CardContent>
</CardActionArea>
<CardActions className="ButtonHolder">
<Box className="likesContainer">
{props.likeButtonVisible ? (
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
) : (
""
)}
<Typography
style={{ marginLeft: props.likeButtonVisible ? 0 : "0.2vmax" }}
>
{likedNumbers}
</Typography>
</Box>
{props.likeButtonVisible ? (
<ToggleIcon
on={saved}
onIcon={
<BookmarkOutlinedIcon onClick={() => unsave(props.idData)} />
}
offIcon={
<BookmarkBorderIcon onClick={() => save(props.idData)} />
}
/>
) : (
""
)}
</CardActions>
</Card>
<Dialog
maxWidth="md"
onClose={handleClose}
aria-labelledby="MoreInfo"
open={open}
>
<DialogTitle id="MoreInfo" onClose={handleClose}>
{props.title}
</DialogTitle>
<DialogContent dividers>
{props.images[0].url ? (
<Carousel infiniteLoop="true">
{props.images.map((el) => {
return (
<div key={Math.random()}>
<img alt="" src={"http://localhost:5000/image/" + el.url} />
<p className="legend">{el.caption}</p>
</div>
);
})}
</Carousel>
) : (
""
)}
<Typography gutterBottom>
<Typography>
<b>Категория: </b>
{category(props.category)}
<b> Опасно: </b>
{dangerous(props.dangerous)}
<b> Цена: </b>
{price(props.price)} <b> Достъпност: </b>
{accessibility(props.accessibility)}
</Typography>
{props.description}
</Typography>
</DialogContent>
<DialogActions className="btnCard">
<Box className="likesContainer">
{props.likeButtonVisible ? (
<ToggleIcon
on={liked}
onIcon={
<FavoriteOutlinedIcon onClick={() => unlike(props.idData)} />
}
offIcon={
<FavoriteBorderIcon onClick={() => like(props.idData)} />
}
/>
) : (
""
)}
<Typography
style={{ marginLeft: props.likeButtonVisible ? 0 : "0.2vmax" }}
>
{likedNumbers == 0
? "Няма харесвания"
: likedNumbers == 1
? "1 харесване"
: likedNumbers + " харесвания"}
</Typography>
</Box>
<Box>
{props.likeButtonVisible ? (
<ToggleIcon
on={saved}
onIcon={
<BookmarkOutlinedIcon onClick={() => unsave(props.idData)} />
}
offIcon={
<BookmarkBorderIcon onClick={() => save(props.idData)} />
}
/>
) : (
""
)}
<ShareOutlinedIcon />
{props.reportButtonVisible ? <ReportOutlinedIcon /> : ""}
</Box>
</DialogActions>
</Dialog>
</div>
);
};
export default CardElement;
更新 1
经过几次实验,我相当确定问题出在按钮(动画)的缓慢变化导致触发其他功能,例如喜欢而不是不同,反之亦然。我正在尝试找到解决这个问题的方法。
更新 2 通过创建一个包装函数来解决问题,该包装函数调用正确的 like/unlike 函数,而不管存在哪个按钮。
wrapper函数调用正确的like unlike函数解决了动画引起的问题
我建议查看 RxJS lib 并限制对 api 的请求数量(这将提高浏览器性能)。 查看下面的简短片段:
// Create a subject which will store the latest value
const buttonClicked = new Subject<{itemId: string, isLiked: boolean}>();
// Debounce for 200 sec.
// debounceTime - Emits a notification from the source Observable only after a particular time span has passed without another source emission. (ref: https://rxjs.dev/api/operators/debounceTime)
const buttonClickedDebounced = buttonClicked.pipe(debounceTime(200));
// If no click was triggered due the time, execute the call to api with latest params.
buttonClickedDebounced.subscribe(({itemId: string, isLiked: boolean}) =>
{
axios.request({...})
// After you get a response it's nice to update the number or likes as well, as soon the other User could like/unlike the same card.
}
);
// Register the click event
function like(itemId, isLiked) {
setLiked(isLiked)
buttonClicked.next({itemId, isLiked})
}
使用这种方法,您将仅向 api 发送 1 个请求,其中包含用户在多次点击后决定离开(喜欢或不喜欢)的最新状态。 希望这有助于改进您的申请!