React hooks - useCallback for defining event handlers

An interesting problem with a React function component.

I had created a continuous scrolling image gallery function, which took as input (from parent props):

  • an array of image URLs
  • a "fetch more" function reference (for when the user has scrolled to the bottom of the window, and the gallery needs to ask the parent component to fetch more images)
  • a boolean value to store whether the parent component is in the process of fetching more images
  • a boolean value to store whether the last call to the server server reported that there were no more images available to load after the current batch

Inside the gallery component, I had a window event handler that evaluates the scroll position, to figure out whether the user has scrolled down to the bottom of the window.  When the user has scrolled to the bottom of the window, it evaluates whether (a) the parent is already fetching more images, (b) whether the server last reported that there were more images to fetch. If (a) is false, and (b) is true - it calls the method on the parent component to fetch the next batch of images (sending what it believes to be the Id of the last image in the array as a property).

My function component code looked a bit like this:

function ImageGallery(props)
{
  React.useEffect(() => {
    window.addEventListener('scroll', handleWindowScroll);
     return () => { window.removeEventListener('scroll', handleWindowScroll);}
  },[]);

   function handleWindowScroll(event)
{
    const { Images, LoadingMore, IsMore } = props;
... }
    return <div>Stuff</div>;
}

However, when the handleWindowScroll event handler was called, the props it had available seemed to be out of date.  It always thought there were 20 images in the array, that LoadingMore was set to false, and IsMore set to true (which is the default state for the application).

I did a bit of reading on this - and apparently its because each time your component renders, a new instance is created of any functions that sit within it.  The event handler is tied to the original instance of the function, hence why when it fires, it has access to the props that were available at the time it was defined/bound. 

The answer is to use a combination of useEffect and useCallback.

function ImageGallery(props)
{
    const handleWindowScroll = React.useCallback((event) => {
        const { Images, LoadingMore, IsMore } = props;
...
    },[props.LoadingMore, props.IsMore, props.Images.length]);

    React.useEffect(() => {
        window.addEventListener('scroll', handleWindowScroll);
        return () => { window.removeEventListener('scroll', handleWindowScroll);}
    },[handleWindowScroll]);
return <div>Stuff</div>;
}

The above code updates handleWindowScroll, only when the values of LoadingMore, IsMore (or the length of the Images array) changes.

At the same time, this causes a useEffect() handler to fire (because handleWindowScroll has changed).  The code inside the useEffect() causes the event listener to rebind to the new handler (first, unbinding from the old handler, and then binding to the new handler).

This works fine.  

An important lesson - you often think you've learnt all there is to know about React function components (or enough to do what you need) - but you spend an afternoon solving an issue like this, and you realise you need to learn quite a bit more.  As always in IT and software engineering!

Comments