skip to Main Content
'use client'

import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Observer } from 'gsap-trial/Observer'
import gsap from '@/utils/gsap'

type Props = {}

const TestPage = (props: Props) => {
  const [test, setTest] = useState<number>(0)

  const testFunc = useCallback(() => {
    console.log(test)
    setTest(prev => prev + 1)
  }, [test])

  const handleClick = () => console.log(test)

  useEffect(() => {
    window.addEventListener('scroll', testFunc)
  }, [])

  return (
    <>
      <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
      <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
      <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
      <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
      <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
    </>
  )
}

export default TestPage

Can someone help me explain why the console.log(test) inside the testFunc always returns 0? I’ve checked using useEffect to see if the state is updated, and it does update everytime the testFunc run.

3

Answers


  1. The problem is that useEffect and useCallback are missing the necessary dependencies. This is why your values ​​are not updating correctly. This happens because the values ​​inside the callbacks remain old. Also a recommendation to clear all event listeners in useEffect. I give an example of the component below.

    const TestPage = () => {
      const [test, setTest] = useState(0)
    
      const testFunc = useCallback(() => {
        console.log(test)
        setTest(prev => prev + 1)
      }, [setTest, test])
    
      const handleClick = () => console.log(test)
    
      useEffect(() => {
        window.addEventListener('scroll', testFunc)
        return () => {
          window.removeEventListener('scroll', testFunc)
        }
      }, [testFunc])
    
      return (
        <>
          <div style={{height: '100vh'}} onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
        </>
      )
    }
    
    Login or Signup to reply.
  2. The issue you’re facing is related to closures and the asynchronous nature of the testFunc. When you use useEffect to add an event listener to the scroll event and pass testFunc as the callback, it captures the initial value of test (which is 0 in your case) in a closure.

    'use client';
    
    import React, { useCallback, useEffect, useState } from 'react';
    
    type Props = {};
    
    const TestPage = (props: Props) => {
      const [test, setTest] = useState<number>(0);
    
      const testFunc = useCallback(() => {
        console.log(test);
        setTest((prev) => prev + 1);
      }, [test]);
    
      const handleClick = () => console.log(test);
    
      useEffect(() => {
        window.addEventListener('scroll', testFunc);
        return () => {
          window.removeEventListener('scroll', testFunc);
        };
      }, [testFunc]);
    
      return (
        <>
          <div className='h-screen bg-black' onClick={handleClick}>
            TestPage
          </div>
          <div className='h-screen bg-black' onClick={handleClick}>
            TestPage
          </div>
          <div className='h-screen bg-black' onClick={handleClick}>
            TestPage
          </div>
          <div className='h-screen bg-black' onClick={handleClick}>
            TestPage
          </div>
          <div className='h-screen bg-black' onClick={handleClick}>
            TestPage
          </div>
        </>
      );
    };
    
    export default TestPage;
    

    You can use the useCallback hook to memoize the testFunc function so in this the testFunc function is recreated whenever the test state changes, preventing the closure issue.

    Login or Signup to reply.
  3. This will work for your case as you want. Just add testFunc as a dependency in use effect and don’t need to use a callback.

      
    'use client'
    
    import React, { useCallback, useEffect, useRef, useState } from 'react'
    import { Observer } from 'gsap-trial/Observer'
    import gsap from '@/utils/gsap'
    
    type Props = {}
    
    const TestPage = (props: Props) => {
      const [test, setTest] = useState<number>(0)
    
      const testFunc = () => {
        console.log(test)
        setTest(prev => prev + 1)
      }
    
      const handleClick = () => console.log(test)
    
      useEffect(() => {
        window.addEventListener('scroll', testFunc)
        return () => {
          window.removeEventListener('scroll', testFunc);
        };
      }, [testFunc])
    
      return (
        <>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
          <div className='h-screen bg-black' onClick={handleClick}>TestPage</div>
        </>
      )
    }
    
    export default TestPage
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search