tdranv
tdranv

Reputation: 1340

React: save ref to state in a custom hook

I want to create a ref to an element, save it in state and use it somewhere else, down the line. Here is what I have so far:

const Header = () => {
  const topElement = useRef();
  const { setRootElement } = useScrollToTop();

  useEffect(() => {
    setRootElement(topElement);
  }, []);

  return (
    <div ref={topElement}>
     ...
    </div>
  )
}

The useScrollToTop hook:

export const useScrollToTop = () => {
  const [rootElement, setRootElement] = useState();

  const scrollToTop = () => {
    rootElement.current.scrollIntoView();
  };

  return {
    scrollToTop: scrollToTop,
    setRootElement: setRootElement
  };
};

And in a different component:

const LongList = () => {

    const { scrollToTop } = useScrollToTop();

    return (
       <div>
        ....
          <button onClick={() => scrollToTop()} />
       </div>
    );
}

The setRootElemet works okay, it saves the element that I pass to it but when I call scrollToTop() the element is undefined. What am I missing here?

Upvotes: 2

Views: 4730

Answers (2)

smashed-potatoes
smashed-potatoes

Reputation: 2222

As hooks are essentially just functions, there is no state shared between calls. Each time you call useScrollToTop you are getting a new object with its own scrollToTop and setRootElement. When you call useScrollToTop in LongList, the returned setRootElement is never used and therefore that instance rootElement will never have a value.

What you need to do is have one call to useScrollToTop and pass the returned items to their respective components. Also, instead of using a state in the hook for the element, you can use a ref directly and return it.

Putting these together, assuming you have an App structure something like:

  • App
    • Header
    • LongList

Hook:

export const useScrollToTop = () => {
  const rootElement = useRef();

  const scrollToTop = () => {
    rootElement.current.scrollIntoView();
  };

  return {
    scrollToTop,
    rootElement,
  };
};

App:

...
const { scrollToTop, rootElement } = useScrollToTop();

return (
  ...
  <Header rootElementRef={rootElement} />
  <LongList scrollToTop={scrollToTop} />
  ...
);

Header:

const Header = ({ rootElementRef }) => {
  return (
    <div ref={rootElementRef}>
     ...
    </div>
  );
}

LongList:

const LongList = ({ scrollToTop }) => {
  return (
    <div>
    ...
      <button onClick={() => scrollToTop()} />
    </div>
  );
}

Upvotes: 1

Shubham Jain
Shubham Jain

Reputation: 930

The issue probably is topElement would be null initially and useEffect would trigger setRootElement with null. You would need to keep topElement in state variable and check when it changes and set the value inside your JSX as

const [topElement, setTopElement] = useState(null);

useEffect(() => {topElement && setRootElement(topElement);}, [topElement])

return (
    <div ref={(ref) => setTopElement(ref)}>
         ...
    </div>
);

Upvotes: 0

Related Questions