React Hooks 多个带有单独倒计时的警报
React Hooks multiple alerts with individual countdowns
我一直在尝试构建一个具有多个警报的 React 应用程序,这些警报会在设定的时间后消失。示例:https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
问题是如果我创建多个警报,它会以错误的顺序消失。例如,测试 0、测试 1、测试 2 应该从测试 0、测试 1 等开始消失,但测试 1 首先消失,测试 0 最后消失。
我一直看到对 useRefs 的引用,但我的实现没有解决这个错误。
有了@ehab 的意见,我相信我能够朝着正确的方向前进。我在我的代码中收到了关于添加依赖项的进一步警告,但额外的依赖项会导致我的代码出现错误。最终我想出了如何使用 refs。我将其转换为自定义挂钩。
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
好吧,你的问题是你在每个 re-render 上重新安装,所以基本上你在渲染时为所有组件重置计时器。
为了清楚起见,请尝试在警报组件中添加 {Date.now()}
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
你每次都会注意到重置
因此,要在功能组件中实现这一点,您需要使用 React.memo
使您的代码正常工作的示例:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
第 2 次修复您的 useEffect,使其不在每个渲染器上 运行 清理功能
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
最后是关于品味的东西,但你真的需要破坏 {...m}
对象吗?我会把它作为一个适当的道具传递,以避免每次都创建新对象!
你的代码有两处错误,
1) 你使用 effect 的方式意味着每次渲染组件时都会调用这个函数,但是显然根据你的用例,你希望这个函数被调用一次,所以把它改成
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
将空数组添加为第二个参数,意味着您的效果不依赖于任何参数,因此只应调用一次。
您的删除警报取决于创建函数时捕获的值,这是有问题的,因为那时您没有数组中的所有警报,将其更改为
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
这是你的样本在我分叉后工作
https://codesandbox.io/s/multiple-alert-countdown-02c2h
这两个答案都遗漏了问题的几点,所以经过一段时间的挫折弄清楚这个问题后,这就是我得出的方法:
- 有一个管理“警报”数组的挂钩
- 每个“警报”组件管理其自身的销毁
但是,由于函数会随着每次渲染而变化,计时器会在每次道具更改时重置,这至少可以说是不可取的。
如果您试图遵守 eslint 详尽的 deps 规则,它还会增加另一层复杂性,您应该这样做,否则您将遇到状态响应问题。其他建议,如果您正在使用“useCallback”,那您找错地方了。
在我的例子中,我使用的是超时的“叠加层”,但您可以将它们想象成警报等。
打字稿:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
然后渲染组件的位置:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
基本上:每个“叠加层”都有一个唯一的 ID。每个“overlay”组件管理自己的销毁,overlay 通过 prop 函数与 overlayManger 通信,然后 eslint exhaustive-deps 通过在 overlay 组件中设置一个“alive”状态 属性 来保持快乐,当更改为 false 时,将调用它自己的销毁。
我一直在尝试构建一个具有多个警报的 React 应用程序,这些警报会在设定的时间后消失。示例:https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
问题是如果我创建多个警报,它会以错误的顺序消失。例如,测试 0、测试 1、测试 2 应该从测试 0、测试 1 等开始消失,但测试 1 首先消失,测试 0 最后消失。
我一直看到对 useRefs 的引用,但我的实现没有解决这个错误。
有了@ehab 的意见,我相信我能够朝着正确的方向前进。我在我的代码中收到了关于添加依赖项的进一步警告,但额外的依赖项会导致我的代码出现错误。最终我想出了如何使用 refs。我将其转换为自定义挂钩。
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
好吧,你的问题是你在每个 re-render 上重新安装,所以基本上你在渲染时为所有组件重置计时器。
为了清楚起见,请尝试在警报组件中添加 {Date.now()}
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
你每次都会注意到重置
因此,要在功能组件中实现这一点,您需要使用 React.memo
使您的代码正常工作的示例:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
第 2 次修复您的 useEffect,使其不在每个渲染器上 运行 清理功能
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
最后是关于品味的东西,但你真的需要破坏 {...m}
对象吗?我会把它作为一个适当的道具传递,以避免每次都创建新对象!
你的代码有两处错误,
1) 你使用 effect 的方式意味着每次渲染组件时都会调用这个函数,但是显然根据你的用例,你希望这个函数被调用一次,所以把它改成
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
将空数组添加为第二个参数,意味着您的效果不依赖于任何参数,因此只应调用一次。
您的删除警报取决于创建函数时捕获的值,这是有问题的,因为那时您没有数组中的所有警报,将其更改为
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
这是你的样本在我分叉后工作 https://codesandbox.io/s/multiple-alert-countdown-02c2h
这两个答案都遗漏了问题的几点,所以经过一段时间的挫折弄清楚这个问题后,这就是我得出的方法:
- 有一个管理“警报”数组的挂钩
- 每个“警报”组件管理其自身的销毁
但是,由于函数会随着每次渲染而变化,计时器会在每次道具更改时重置,这至少可以说是不可取的。
如果您试图遵守 eslint 详尽的 deps 规则,它还会增加另一层复杂性,您应该这样做,否则您将遇到状态响应问题。其他建议,如果您正在使用“useCallback”,那您找错地方了。
在我的例子中,我使用的是超时的“叠加层”,但您可以将它们想象成警报等。
打字稿:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
然后渲染组件的位置:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
基本上:每个“叠加层”都有一个唯一的 ID。每个“overlay”组件管理自己的销毁,overlay 通过 prop 函数与 overlayManger 通信,然后 eslint exhaustive-deps 通过在 overlay 组件中设置一个“alive”状态 属性 来保持快乐,当更改为 false 时,将调用它自己的销毁。