如何在useEffect中访问状态而无需重新触发useEffect?

时间:2020-08-03 05:25:20

标签: javascript reactjs react-hooks

我需要添加一些与React之外的对象进行交互的事件处理程序(以Google Maps为例)。 在此处理函数中,我想访问一些可以发送给该外部对象的状态。

如果将状态作为对效果的依赖关系进行传递,则它可以工作(我可以正确访问状态)。但是每次状态更改时都会添加添加/删除处理程序。

如果我没有将状态作为依赖项传递,则添加/删除处理程序将添加适当的时间(基本上是一次),但是状态永远不会更新(或更准确地说,处理程序无法拉最新状态)。

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"));

注意到状态更改后,添加/删除处理程序仅被添加一次(显然忽略了未卸载App组件的事实)。

我正在寻找一种用钩子复制它的方法

5 个答案:

答案 0 :(得分:1)

正在发生的是,该函数在n上关闭,但是闭包通常会看到对变量的更新,而钩子变量会一直在重新创建,从而使闭包变老。

在基于钩子的组件中,状态在每个渲染器上分配了一个新变量,侦听器函数从未关闭过该变量,并且闭包不会被更新,因为仅在挂载时创建函数一次(使用空的依赖项数组) )。相反,在基于类的组件中,this保持不变,因此闭包可以看到更改。

我认为不会不断增加和删除侦听器。考虑一下事实,除非您在日常的React事件中使用useCallback()创建事件处理程序(您只应对记忆的孩子进行此操作,否则,这是过早的优化),React本身实际上就是这样做,即删除前一个功能并设置新功能。

答案 1 :(得分:1)

您可以使用mutable refs来使当前状态与效果依赖项脱钩:

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上下文-这就是为什么类组件示例的this.state.n中的handleMouseOver总是返回最新状态并起作用的原因。

上面的setInterval模式展示了一个很棒的example by Dan Abramov。博客文章还说明了useCallback的潜在问题以及每次状态更改时都读取/删除事件侦听器的情况。

其他有用的示例是os.addEventListener之类的(全局)事件处理程序,或与React边缘的外部库/框架集成。

注意: React文档推荐给use this pattern sparingly。从我的角度来看,在您只需要“最新状态”(独立于React渲染周期更新)的情况下,它是一个可行的选择。通过使用可变变量,我们可以使用可能过时的闭包值突破函数闭包的范围。

状态独立于依赖项还有其他选择-您可以查看How to register event with useEffect hooks?了解更多信息。

答案 2 :(得分:0)

您可以使用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

答案 3 :(得分:0)

获取最新值的唯一方法是将其指定为依赖项,这是背后的原因

  1. 为什么一次又一次添加或删除调用?

    每次依赖项更改时,它都会重新执行整个功能

  2. 为什么n的值未更新?

    每次渲染功能组件时,所有分配都将像普通函数一样重新发生,因此存储“ n = 0”的参考对象的值将保持不变,并在随后的每个渲染对象上创建一个新对象。将指向更新后的值

答案 4 :(得分:0)

您尝试解决此特定问题的方式中存在一些问题。

如果我将状态作为对效果的依赖关系进行传递,则它将起作用(我可以 正确访问状态),但每次添加添加/删除处理程序 状态改变的时候。

之所以有效,是因为处理程序功能已根据acc更新。到调用n时的最新useeffect值。

如果我不将状态作为依赖项传递,则添加/删除处理程序为 添加了适当的时间(基本上是一次),但是 状态永远不会更新(或更准确地说,处理程序无法拉出 最新状态)。

这是因为处理程序函数未获取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>
  );
}