为什么在React中handleScroll中的状态(state)不起作用?

huangapple go评论71阅读模式
英文:

Why is state not working in handleScroll in React?

问题

由于某种原因,在“handleScroll”函数中访问“height”或“scrollPos”时,它们具有它们的初始值。但是,如果我添加一个带有“{height}”的“p”标签,它会正确显示。

const [scrollPos, setScrollPos] = useState(0);
const [height, setHeight] = useState(0);
const [top, setTop] = useState(0);
const ref = useRef(null);

useEffect(() => {
  setHeight(ref?.current?.clientHeight);
  setScrollPos(-window.pageYOffset);
  window.addEventListener("scroll", handleScroll);
}, []);

const handleScroll = () => {
  const x = -window.pageYOffset;

  const dif = x - scrollPos;

  setTop(top + dif > 0 ? 0 : top + dif < -height ? -height : top + dif);
  setScrollPos(x);
};

我正在将组件从CC(类组件)转换为RFC(函数组件),以前它是有效的。

英文:

For some reason, when I acces 'height' or 'scrollPos' in the 'handleScroll' function, they have their initial value. However, if I add a 'p' tag with '{height}', it is displayed correctly.

const [scrollPos, setScrollPos] = useState(0);
  const [height, setHeight] = useState(0);
  const [top, setTop] = useState(0);
  const ref = useRef(null);

  useEffect(() =&gt; {
    setHeight(ref?.current?.clientHeight);
    setScrollPos(-window.pageYOffset);
    window.addEventListener(&quot;scroll&quot;, handleScroll);
  }, []);

  const handleScroll = () =&gt; {
    const x = -window.pageYOffset;

    const dif = x - scrollPos;

    setTop(top + dif &gt; 0 ? 0 : top + dif &lt; -height ? -height : top + dif);
    setScrollPos(x);
  };

I am converting the component from a CC to a RFC, and it worked before.

答案1

得分: 1

问题出在handleScroll的状态在事件触发时从不改变。因此,事件的回调需要在每次需要更新当前状态时重新创建。

这里是一个解决方案:

在创建事件的useEffect的依赖数组中传递要使用的状态变量。为了避免每次调用useEffect回调时都复制事件,需要将其删除。你可以使用函数的返回回调来实现这一点。

以下是使用这种解决方案后你新的 hooks 将会是这样的:

  useEffect(() => {
    setHeight(ref?.current?.clientHeight);
    setScrollPos(-window.pageYOffset);
  }, []);

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [height, scrollPos, top]);

这样,每当这三个值中的一个更新时,事件就会被重新创建。

更新

正如你评论中提到的,另一种方法是直接将函数 handleScroll 放在 useEffect 的依赖数组中。但在这种情况下,我建议你使用 useCallback 钩子,它只会在依赖数组中的某个值发生变化时重新计算函数(否则它将在每次组件重新渲染时进行计算,这经常发生)。

 const handleScroll = useCallback(() => {
    // ...
  }, [top, height, scrollPos]);

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [handleScroll]);

为什么这样能行?

我认为解释这个的最好方法是将组件的不同状态视为快照。每当值发生变化时,就会创建一个新的快照,使用状态的值重写每个函数的实现,重新渲染模板等等。

因此,每次状态更新时,组件的函数的实现将使用新值重新计算。这意味着,如果我在组件中有这样一个状态:

const [count, setCount] = useState(0);

还有一个这样的函数:

function getCount(){
  return count; // 假设 count 是在其中的
}

实际上会在快照中计算如下:

function getCount(){
  return 0;
}

因此,当你将 handleScroll 函数作为 window.addEventListener 的回调传递时,你实际上是传递了一个根据组件状态的特定值计算的函数,因此回调永远不会更新。这就是为什么这种方法能够奏效的原因,你实际上是每次都传递了一个不同的函数!

英文:

The problem comes from the fact that the state of handleScroll never changes when it's triggered by the event. So the callback of the event needs to be recreated every time it is needed to have the current state updated inside the implementaiton of the callback.

Here's a solution:

In the dependency array of the useEffect creating the event, pass the state variables to be used. And, to avoid the event to be duplicated every time the useEffect callback is called, it needs to be removed. For this you can use the return callback of the function

Here what your new hooks will looks like with this solution:

  useEffect(() =&gt; {
    setHeight(ref?.current?.clientHeight);
    setScrollPos(-window.pageYOffset);
  }, []);


  useEffect(() =&gt; {
    window.addEventListener(&quot;scroll&quot;, handleScroll);
    return () =&gt; window.removeEventListener(&quot;scroll&quot;, handleScroll);
  }, [height, scrollPos, top]);

That way, the event will be recreated everytime one of the three values is updated.

Update

As you commented, another way to do it is to pass directly the function handleScroll inside the dependencies array of the useEffect. But in that case I recommand you to use the useCallback hook that will recompute the function only if a value inside the dependencies array has changed (else it will be at each rerender of the component, and that happen a lot)

 const handleScroll = useCallback(() =&gt; {
    // ...
  }, [top, height, scrollPos]);

  useEffect(() =&gt; {
    window.addEventListener(&quot;scroll&quot;, handleScroll);
    return () =&gt; window.removeEventListener(&quot;scroll&quot;, handleScroll);
  }, [handleScroll]);

Why does this works ?

I think the best way to explain this is to see the differents states of a component as snapshot. Each time a value change, a new snapshot is created, rewritting the implementation of every function with the values of the states, rerendering the template etc...

So, everytime the state is updated, the implementation of the function of the component will be recomputed with the new values.
Meaning that if i have this state in a component:

const [count, setCount] = useState(0);

And a function like this:

function getCount(){
  return count; // Let&#39;s supposed that count is in th
}

Will actually be computed like this in the snapshot :

function getCount(){
  return 0;
}

So, when you pass the handleScroll function as a callback of window.addEventListener, you are actually passing a function computed with a specific value of the state of the component, so the callback will never be up to date.

That is way doing this method works, you are actually passing a different function everytime !

huangapple
  • 本文由 发表于 2023年7月3日 19:39:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76604381.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定