【问题标题】:How can I access state in an useEffect without re-firing the useEffect?如何在不重新触发 useEffect 的情况下访问 useEffect 中的状态?
【发布时间】:2020-11-23 04:59:38
【问题描述】:

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

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

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

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 组件未卸载的事实),尽管状态发生了变化。

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

【问题讨论】:

  • 使用ref 而不是state。它不漂亮,但你在这里想要做的也不是。
  • 谢谢托马斯。您能否详细说明上述方法的哪些地方不“漂亮”?在基于类的 React 方法中,它只是 componentDidMountcomponentWillUnmount 。我正在尝试复制设置和清理的行为。
  • How to register event with useEffect hooks? 包含访问useEffect 内部状态的所有不同方法的详细比较。让我知道,如果这对你有用。
  • @Chris 在这里给出了答案,以更好地适应您的阅读状态。希望,这有帮助
  • @Mordechai 第三个和第四个选项都可以用于读取状态。虽然这确实与其他答案有所不同 - 感谢您提及。

标签: javascript reactjs react-hooks


【解决方案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 上下文 - 这就是为什么类组件示例的 handleMouseOver 中的 this.state.n 始终返回最新状态并正常工作。

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

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

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

编写独立于依赖的状态有更多的选择——你可以查看How to register event with useEffect hooks?了解更多信息。

【讨论】:

  • 谢谢@ford04。这个,以及你在另一篇文章中的回答正是我所追求的。我需要再等 20 个小时才能申请奖励,但届时我会这样做。干杯队友
【解决方案2】:

发生的情况是,函数在 n 上关闭,但是虽然闭包通常会看到对变量的更新,但挂钩变量会一直在重新创建,从而使闭包失效。

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

我不认为不断添加和删除侦听器是一个问题。考虑一个事实,除非你在日常反应事件中使用useCallback() 来创建你的事件处理程序(你应该只对记忆的孩子做,否则它是过早的优化),React 本身就会这样做,即删除以前的函数和设置新功能。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 如果组件在应用中被多次使用怎么办?
    【解决方案4】:

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

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

      每次依赖项发生变化时,它都会重新执行整个函数

    2. 为什么n 的值没有更新?

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

    【讨论】:

      【解决方案5】:

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

      如果我将状态作为依赖项传递给效果,它可以工作(我可以 正确访问状态),但添加/删除处理程序每​​次添加 状态变化的时间。

      这行得通,因为处理函数是根据更新的。当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>
        );
      }
      

      【讨论】:

        猜你喜欢
        • 2022-12-30
        • 1970-01-01
        • 2023-01-26
        • 2021-07-03
        • 1970-01-01
        • 1970-01-01
        • 2020-05-16
        • 2022-01-08
        • 1970-01-01
        相关资源
        最近更新 更多