我需要添加一些与React之外的对象交互的事件处理程序(以谷歌地图为例)。在这个处理程序函数中,我想访问一些可以发送到这个外部对象的状态。
如果我将状态作为依赖项传递给效果,它会起作用(我可以正确访问状态),但每次状态更改时都会添加添加/删除处理程序。
如果我不将状态作为依赖项传递,那么添加/删除处理程序会被添加适当的次数(基本上是一次),但状态永远不会更新(或者更准确地说,处理程序无法获取最新状态)。
Codepen示例:
也许最好用Codepen来解释:https://codepen.io/cjke/pen/dyMbMYr?editors=0010
const App = () => {
const [n, setN] = React.useState(0);
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver = () => {
// I know innerHTML isn't "react" - this is an example of interacting with an element outside of React
os.innerHTML = `N=${n}`
}
console.log('Add handler')
os.addEventListener('mouseover', handleMouseOver)
return () => {
console.log('Remove handler')
os.removeEventListener('mouseover', handleMouseOver)
}
}, []) // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked
return (
<div>
<button onClick={() => setN(n + 1)}>+</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
摘要
如果效果的dep列表是[n]
,则会更新状态,但每次状态更改都会添加/删除添加/删除处理程序。如果效果的dep列表是[]
,则添加/删除处理程序工作正常,但状态始终为0(初始状态)。
我想要两者的混合物。访问状态,但只访问useEffect一次(就好像依赖项是[]
一样)。
编辑:进一步澄清
我知道如何使用生命周期方法来解决它,但不确定如何使用Hooks。
如果上面是一个类组件,它看起来像:
class App extends React.Component {
constructor(props) {
super(props)
this.state = { n: 0 };
}
handleMouseOver = () => {
const os = document.getElementById("outside-react");
os.innerHTML = `N=${this.state.n}`;
};
componentDidMount() {
console.log("Add handler");
const os = document.getElementById("outside-react");
os.addEventListener("mouseover", this.handleMouseOver);
}
componentWillUnmount() {
console.log("Remove handler");
const os = document.getElementById("outside-react");
os.removeEventListener("mouseover", handleMouseOver);
}
render() {
const { n } = this.state;
return (
<div>
<strong>Info:</strong> Click button to update N in state, then hover the
orange box. Open the console to see how frequently the handler is
added/removed
<br />
<button onClick={() => this.setState({ n: n + 1 })}>+</button>
<br />
state inside react: {n}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
注意添加/删除处理程序如何只添加一次(显然忽略了应用程序组件没有卸载的事实),尽管状态发生了变化。
我正在寻找一种方法来复制与挂钩
您可以使用可变引用将读取当前状态与效果依赖关系解耦:
const [n, setN] = useState(0);
const nRef = useRef(n); // define mutable ref
useEffect(() => { nRef.current = n }) // nRef is updated after each render
useEffect(() => {
const handleMouseOver = () => {
os.innerHTML = `N=${nRef.current}` // n always has latest state here
}
os.addEventListener('mouseover', handleMouseOver)
return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies
const App = () => {
const [n, setN] = React.useState(0);
const nRef = React.useRef(n); // define mutable ref
React.useEffect(() => { nRef.current = n }) // nRef.current is updated after each render
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver = () => {
os.innerHTML = `N=${nRef.current}` // n always has latest state here
}
os.addEventListener('mouseover', handleMouseOver)
return () => { os.removeEventListener('mouseover', handleMouseOver) }
}, []) // no need to set dependencies
return (
<div>
<button onClick={() => setN(prev => prev + 1)}>+</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<div id="outside-react">div</div>
<p>Update counter with + button, then mouseover the div to see recent counter state.</p>
在装载/卸载时,只会添加/删除一次事件侦听器。当前状态n
可以在useEffect
内部读取,而无需将其设置为依赖项([]
deps),因此不会在更改时重新触发。
您可以将useRef
视为函数组件和Hooks的可变实例变量。类组件中的等效组件是this
上下文,这就是为什么类组件示例的handleMouseOver
中的this.state.n
总是返回最新状态并工作的原因。
Dan Abramov的一个很好的例子用setInterval
展示了上述模式。这篇博客文章还说明了useCallback
的潜在问题,以及每次状态更改时事件侦听器被读取/删除的情况。
其他有用的例子是(全局)事件处理程序,如os.addEventListener
或与React边缘的外部库/框架的集成。
注意:React文档建议谨慎使用此模式。在我看来,在你只需要";最新状态"-独立于React渲染周期更新。通过使用可变变量,我们可以使用可能过时的闭包值来突破函数闭包范围。
独立于依赖项编写状态还有其他选择-您可以看看如何使用useEffect挂钩注册事件?了解更多信息。
实际情况是,函数在n
上关闭,但虽然闭包通常会看到变量的更新,但钩子变量在闭包之后一直在重新创建。
在基于钩子的组件中,状态在每个呈现上都被分配了一个新变量,侦听器函数从未关闭过该变量,并且关闭也不会更新,因为您只在装载时创建了一次函数(使用空的依赖数组)。相反,在基于类的组件中,this
保持不变,因此闭包可以看到变化。
我不认为不断添加和删除听众是一个问题。考虑到这样一个事实,除非你在日常的react事件中使用useCallback()
来创建你的事件处理程序(你应该只对记忆化的孩子做这件事,否则这是过早的优化),否则react本身就会真正做到这一点,即删除前一个函数并设置新函数。
获取最新值的唯一方法是将其指定为依赖项,下面是背后的推理
-
为什么添加或删除一次又一次调用?
每次依赖项发生变化时,它都会重新执行整个功能
-
为什么
n
的值没有更新?每次渲染功能组件时,所有赋值都将像正常函数一样重新发生,因此存储"n=0"的引用对象的值将保持不变,并且在每次后续渲染时,将创建新对象,该对象将指向更新值
您试图解决这个特定问题的方式存在一些问题。
如果我将状态作为依赖项传递给效果,它就会起作用(我可以正确访问状态),但是添加/删除处理程序每隔状态改变的时间。
这很有效,因为调用useeffect
时,处理程序函数会根据最新的n
值进行更新。
如果我不将状态作为依赖项传递,则添加/删除处理程序为添加了适当的次数(基本上是一次),但状态永远不会更新(或者更准确地说,处理程序无法提取最新状态)。
这是因为处理程序函数没有获得n
的当前值
在这里使用refs
可能是一个优势,因为该值将在没有转发器的情况下持续存在。请在此处查看此示例:https://codesandbox.io/s/wispy-pond-j80j7?file=/src/App.js
export default function App() {
const [n, setN] = React.useState(0);
const nRef = React.useRef(0);
const outsideReactRef = React.useRef(null);
const handleMouseOver = React.useCallback(() => {
outsideReactRef.current.innerHTML = `N=${nRef.current}`;
}, []);
React.useEffect(() => {
outsideReactRef.current = document.getElementById("outside-react");
console.log("Add handler");
outsideReactRef.current.addEventListener("mouseover", handleMouseOver);
return () => {
console.log("Remove handler");
outsideReactRef.current.removeEventListener("mouseover", handleMouseOver);
};
}, []); // <-- I can change this to [n] and `n` can be accessed, but add/remove keeps getting invoked
return (
<div>
<button
onClick={() =>
setN(n => {
const newN = n + 1;
nRef.current = newN;
return newN;
})
}
>
+
</button>
</div>
);
}
您可以使用window
对象或global
对象来分配您想要的变量,使用useEffect如下:
try{
const App = () => {
const [n, setN] = React.useState(0);
React.useEffect(()=>{
window.num = n
},[n])
React.useEffect(() => {
const os = document.getElementById('outside-react')
const handleMouseOver =() => {
os.innerHTML = `N=${window.num}`
}
console.log('Add handler')
os.addEventListener('mouseover', handleMouseOver)
return () => {
console.log('Remove handler')
os.removeEventListener('mouseover', handleMouseOver)
}
}, []) // <-- I can change this to [n] and it works, but add/remove keeps getting invoked
return (
<div>
<strong>Info:</strong> Click button to update N in state, then hover the orange box. Open the console to see how frequently the handler is added/removed
<br/>
<button onClick={() => setN(n + 1)}>+</button>
<br/>
state inside react: {n}
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
}
catch(error){
console.log(error.message)
}
<div id="outside-react">OUTSIDE REACT - hover to get state</div>
<div id="root"></div>
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
这将改变n状态改变上的window.num