skip to Main Content

Suppose the following:

import * as React from "react";

const useMyHook = ({ element }: { element: HTMLElement | null }) => {
  React.useEffect(() => {
    if (element) {
      console.log("element in hook", element);
    }
  }, [element]);
};

const MyComponent = React.memo(() => {
  const ref = React.useRef<HTMLDivElement | null>(null);

  React.useEffect(() => {
    if (ref.current) {
      console.log("element in parent", ref.current);
    }
  }, [ref.current]);

  useMyHook({ element: ref.current });

  return <div ref={ref}>my content</div>;
});

export default function App() {
  return <MyComponent />;
}

My issue here is that element in hook is never printed to the console. I managed to get it to work by removing React.memo, but I’m wondering why this is happening and how, if possible, I can get this work while still using React.memo?

I know that refs don’t rerender the component, however my hook checks for changes in a useEffect, so I’m a little confused as to why it behaves like that.

const MyComponent = () => {
  // works fine like this
};

3

Answers


  1. The reason why "element in hook" is not printed to the console when using React.memo is because React.memo is a higher-order component that only re-renders the component if its props have changed.

    so you have to use useCallback to memorize the useMyhook

    the code would be:

    import * as React from "react";
    import "./styles.css";
    
    const useMyHook = React.useCallback(({ element }: { element: HTMLElement | null }) => {
      React.useEffect(() => {
        if (element) {
          console.log("element in hook", element);
        }
      }, [element]);
    }, []); // Empty dependency array as useMyHook does not depend on any external variables
    
    const MyComponent = React.memo(() => {
      const ref = React.useRef<HTMLDivElement | null>(null);
    
      React.useEffect(() => {
        if (ref.current) {
          console.log("element in parent", ref.current);
        }
      }, [ref.current]);
    
      useMyHook({ element: ref.current });
    
      return <div ref={ref}>my content</div>;
    });
    
    export default function App() {
      return <MyComponent />;
    }
    
    
    Login or Signup to reply.
  2. In your example the ref gets its value when the div gets mounted.

    <div ref={ref}>my content</div>
    

    This will assign the value to the ref, but your useMyHook runs before that happens. { element: ref.current } element becomes null. It is true that useEffect will call after the render, but the value that is passed to element is passed by value, not by reference. This is the main reason your useEffect inside the component runs as expected, but the useEffect in your hook does not.

    If you do:

    const MyComponent = React.memo(() => {
      const ref = React.useRef<HTMLDivElement | null>(null);
      const element = ref.current  // assign the value of the ref.current
       // in the first render its null, so element = null
    
      React.useEffect(() => {
        if (element) {  // since it is null, it does not pass the condition
          console.log("element in parent", ref.current);
        }
      }, [element]);  // it is null in the first render
    
      return <div ref={ref}>my content</div>;
    });
    

    This wont work either.

    The way you can fix this is by using the ref prop as callback, instead of the result of useRef().

    <div ref={useCallback((node) => console.log(node), [])}>my content</div>;
    

    This callback will be called when the div mounts. The node will be the div itself as HTMLDivElement

    About the React.memo thing, probably something else causes an extra render and that makes { element: ref.current } to give the current value of the div that it had from the previous render.

    Login or Signup to reply.
  3. As Oktay Yuzcan said, the element is passed by value to your hook useMyHook. And because of useMemo your hook is not rerender so it is executed with the value of element before it is actually set.

    An other way to fix your issue is to provide the reference directly to your hook useMyHook instead of the value.

    import * as React from "react";
    
    const useMyHook = ({ element }: { element: RefObject>HTMLElement | null> }) => {
      React.useEffect(() => {
        if (element.current) {
          console.log("element in hook", element.current);
        }
      }, [element.current]);
    };
    
    const MyComponent = React.memo(() => {
      const ref = React.useRef<HTMLDivElement | null>(null);
    
      React.useEffect(() => {
        if (ref.current) {
          console.log("element in parent", ref.current);
        }
      }, [ref.current]);
    
      useMyHook({ element: ref }); // <-- provide directly the reference
    
      return <div ref={ref}>my content</div>;
    });
    
    export default function App() {
      return <MyComponent />;
    }
    
    

    With this way, your hook will receive always the up to date version of element.current.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search