ReactJS:为什么我的自定义功能吐司组件在尝试自动驳回通知弹出窗口时会如此奇怪?
我的自定义React Toast组件运行良好,直到我试图在设定的时间后自动解雇通知。
我正在尝试做到这一点,以便在设定的时间之后,弹出的“烤面包”通知将具有CSS淡出的动画播放,然后删除 - 除非用户悬停在该通知上,否则它将保留它拒绝那个,直到用户将鼠标从中移开为止。
有时它可以正常工作,有时它会停止显示任何内容,并逐一添加通知,而其他时间……好吧,它只是以一种非常奇怪和出乎意料的方式行为。
这是我的代码:
toast.css
.toast-container {
font-size: 24px;
box-sizing: border-box;
position: fixed;
z-index: 10;
}
.toast-popup {
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
width: 500px;
border: solid #f2f2f2;
border-radius: 8px;
box-shadow: 0 0 10px #999;
margin-bottom: 1rem;
opacity: 0.9;
}
.toast-popup:hover {
box-shadow: 0 0 12px deepskyblue;
opacity: 1 !important;
animation-play-state: paused;
}
.success {
background-color: #5cb85c;
}
.info {
background-color: #5bc0de;
}
.warning {
background-color: #f0ad4e;
}
.danger {
background-color: #d9534f;
}
.toast-text {
justify-self: flex-start;
width: 100%;
padding: 6px 0 6px 6px;
opacity: 0.9;
}
.toast-title {
font-weight: 700;
font-size: 32px;
text-align: left;
padding-bottom: 0px;
color: #f2f2f2;
}
.toast-message {
padding-top: 0px;
text-align: left;
color: #f2f2f2;
}
.toast-icon {
float: left;
margin: 0 20px 0 10px;
opacity: 0.9;
}
.toast-icon img {
width: 50px;
height: 50px;
fill: #f2f2f2;
opacity: 0.9;
}
.close-button {
float: right;
align-self: flex-start;
font-weight: 600;
color: #f2f2f2;
background: none;
border: none;
opacity: 0.9;
cursor: pointer;
}
.top-right {
top: 2rem;
right: 2rem;
}
.top-right-slide {
top: 2rem;
right: 2rem;
transition: transform .6s ease-in-out;
animation: toast-in-right .7s;
}
.bottom-right {
bottom: 2rem;
right: 2rem;
}
.bottom-right-slide {
bottom: 2rem;
right: 2rem;
transition: transform .6s ease-in-out;
animation: toast-in-right .7s;
}
.top-left {
top: 2rem;
left: 2rem;
}
.top-left-slide {
top: 2rem;
left: 2rem;
transition: transform .6s ease-in;
animation: toast-in-left .7s;
}
.bottom-left {
bottom: 2rem;
left: 2rem;
}
.bottom-left-slide {
bottom: 2rem;
left: 2rem;
transition: transform .6s ease-in;
animation: toast-in-left .7s;
}
.fadeout {
animation: 4s linear 5s 1 normal forwards running toast-fadeout;
}
@keyframes toast-in-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes toast-in-left {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes toast-fadeout {
from { opacity: 0.9; }
to { opacity: 0; }
}
toast.js - 请原谅console.logs.logs ...
import React, {useEffect, useState} from 'react';
import icon_success from './icons/feathericons/check-circle.svg';
import icon_info from './icons/feathericons/info.svg';
import icon_warning from './icons/feathericons/alert-triangle.svg';
import icon_danger from './icons/feathericons/alert-octagon.svg';
import './Toast.css';
const Toast = (props) => {
const {toastList, position} = props;
const [list, setList] = useState(toastList);
const [prevId, setPrevId] = useState(0);
// This useEffect updates the list of toasts to display
useEffect(() => {
console.log('useEffect()');
console.log('useEffect() toastList:');
console.log(toastList);
setList([...toastList]);
}, [toastList]);
const markForDeletion = (toast) => {
if( toast.isDeleting ) {
return;
}
console.log(`toast ${toast.id} marked for deletion`)
toast.isDeleting = true;
setTimeout(() => {attemptDeletion(toast)}, 5000);
}
const attemptDeletion = (toast) => {
console.log(`attempting to delete toast ${toast.id}. canDelete = ${toast.canDelete}`);
if( toast.canDelete ) {
deleteToast(toast.id);
}
else {
console.log(`cannot delete toast ${toast.id}. `);
}
}
const getIcon = (variant) => {
switch( variant ) {
case 'success':
return icon_success;
break;
case 'info':
return icon_info;
break;
case 'warning':
return icon_warning;
break;
case 'danger':
return icon_danger;
break;
}
}
const generateId = (toast) => {
if( typeof(toast.id) === 'number' ) {
return toast.id;
}
toast.id = prevId + 1;
setPrevId(toast.id);
return toast.id;
}
const deleteToast = (id) => {
console.log(`deleting toast ${id}`);
const deletionIdxList = list.findIndex(e => e.id === id);
const deletionIdxToastList = toastList.findIndex(e => e.id === id);
console.log(`deletionIdxToastList: ${deletionIdxToastList}`);
if(deletionIdxList == null || deletionIdxList === -1) {
console.log(`cannot find list idx of id ${id}`);
console.log('list:');
console.log(list);
return;
}
if(deletionIdxToastList == null || deletionIdxToastList === -1) {
console.log(`cannot find toastList idx of id ${id}`);
console.log('toastList:');
console.log(toastList);
return;
}
console.log('list before deletion:');
console.log(list);
console.log('toastList before deletion:');
console.log(toastList);
console.log('list[deletionIdxList]:');
console.log(list[deletionIdxList]);
list.splice(deletionIdxList, 1);
console.log('toastList[deletionIdxToastList]:');
console.log(toastList[deletionIdxToastList]);
toastList.splice(deletionIdxToastList, 1);
setList([...list]);
console.log(`toast ${id} deleted successfully`);
console.log('list after deletion:');
console.log(list);
console.log('toastList after deletion:');
console.log(toastList);
}
return (
<>
<div className={`toast-container ${position}`} >
{
list.map((toast, i) => (
<div
key={i}
className={`toast-popup ${toast.variant} ${toast.isDeleting ? (position + ' fadeout') : (position + '-slide')}`}
onLoad={() => {
if( !toast.isLoaded ) {
toast.Id = generateId(toast);
toast.canDelete = true;
toast.isDeleting = false;
toast.isLoaded = true;
console.log(`on load ${toast.id}`);
setTimeout(() => markForDeletion(toast), 500);
}
}}
onMouseOver={() => {
toast.canDelete === true ? toast.canDelete = false : null;
toast.isDeleting === true ? toast.isDeleting = false : null;
console.log(`mouse over ${toast.id}`);
}}
onMouseLeave={() => {
toast.canDelete === false ? toast.canDelete = true : null;
markForDeletion(toast);
console.log(`mouse leave ${toast.id}`);
}}
>
<div className={'toast-icon'}>
<img src={getIcon(toast.variant)} />
</div>
<div className={'toast-text'}>
<div className={'toast-title'}>
{toast.variant.charAt(0).toUpperCase() + toast.variant.slice(1)}
</div>
<div className={'toast-message'}>{toast.message}</div>
</div>
<button
className={'close-button'}
onClick={() => {
toast.canDelete = true;
deleteToast(toast.id)
}
}>
X
</button>
</div>
))
}
</div>
</>
)
}
Toast.defaultProps = {
position: 'bottom-right'
}
export default Toast;
home.js我正在测试这个新的吐司组件 - 我正在努力更新预先存在的应用程序,以删除对反应totastify library 的依赖性
// Leaving out constructor and other irrelevant code...
toastSuccess() {
const newToast = {
variant: 'success',
message: 'This is a test of the success variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastInfo() {
const newToast = {
variant: 'info',
message: 'This is a test of the info variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastWarning() {
const newToast = {
variant: 'warning',
message: 'This is a test of the warning variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastDanger() {
const newToast = {
variant: 'danger',
message: 'This is a test of the danger variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
render() {
return (
<div className="Home" style={{height:'100%'}}>
<Toast
toastList={this.state.toastList}
position={'bottom-right'}
/>
<div style={{display:'flex', justifyContent:'center'}}>
<Button onClick={() => this.toastSuccess()}>Success</Button>
<Button onClick={() => this.toastInfo()}>Info</Button>
<Button onClick={() => this.toastWarning()}>Warning</Button>
<Button onClick={() => this.toastDanger()}>Danger</Button>
</div>
{// ...}
</div>
);
}
让我知道是否有一种方法可以使用该代码段功能在Stackoverflow上运行此代码,因为这确实很有帮助,以便您的读者可以直接看到该问题。不幸的是,我从来没有运气过它,但是我会继续尝试一下,看看是否可以弄清楚。
编辑:
感谢 @cristian-florincalina推荐Stackblitz作为一个很好的共享测试环境。我现在在那里设置了它,这里有一个链接: https://react-ts-ts-ybunlg.stackblitz.io
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我在您的代码中看到的第一个问题是,您将两个真相来源保留在吐司列表中。一个通过
props
传递给父,一个是toast
组件中的内部状态列表。这是一个可能产生很多问题的反图案。第二 Big 问题是您正在更改父母收到的列表。这是
React
中的一个巨大的反系列 - 仅阅读“ rel =” nofollow noreferrer”> readonly - 所有反应组件都必须像纯粹的功能相对于其纯粹的功能props。(由于您正在更改数组中的对象,显然它适用于加载更新,但是当您试图在列表中调用剪接时,它也无效 - 这也是为什么即使您删除了元素和应用了删除效果,当它在父上更新(下一个渲染) - &gt;它将返回而不会被删除,然后在另一个吐司生成按钮上再次单击也会向您显示先前删除的吐司)。我认为这里的最大问题是您无法正确使用构图。您不应将敬酒列表传递到
toast
组件,而应保留在父母上的列表,而是将MAP
从父母内部移动。您将每个列表中每个元素的一个实例toast
组件。也许您也可以拥有
toastlist
组件,该组件可以根据其位置处理toast
组件...因此,当您单击上>左上toast Toast Generator 例如,它将在一系列烤面包中添加一个新条目,并带有
将由删除上的position
键。该数组将发送到toastlist
组件,该组件将生成toast
组件,这些组件在内部处理其状态(删除等),并且不更新实际列表。您可以将函数传递到toast
component称为ondelete
,该toast
组件调用,您将更新toastlist
基于这些事件的状态(可能将删除事件传播到父级以更新那里的列表)。希望这是有道理的。
First problem that I see with your code is that you are keeping two sources of truth for toast list. One is passed from the parent via
props
, and one is the internal state list in theToast
component. This is an antipattern that can generate a lot of issues.Second BIG issue is that you are altering the list that you receive from the parent. That is a huge antipattern in
React
sinceprops
are readonly -- All React components must act like pure functions with respect to their props. (since you are altering an object inside an array apparently it works for the load update but it does not work when you are trying to call splice on the list -- this is why even if you deleted the element and applied the deletion effect, when it gets updated on the parent (next render) -> it will come back without it being removed and clicking again on another toast generate button will show you the previously deleted toast as well).I think the big problem here is that you are not using composition properly. Instead of passing the toast list to the
Toast
component, you should keep the list on the parent, move themap
from the child inside the parent. You will have one instance ofToast
component per each element in the list.Maybe you can have a
ToastList
component as well, that handlesToast
compoonents based on their position... So when you click onUpper Left Toast Generator
for example, it will add a new entry inside an array of toasts, with aposition
key. That array will be sent to theToastList
component, which will generateToast
components that handle their state internally (deletion, etc) and do not update the actual list. You can pass a function to theToast
component calledonDelete
that will be called by theToast
component on deletion, and you will update theToastList
state based on those events (probably propagate the delete event to the parent to update the list there).Hope it makes sense.