Search code examples
reactjsrefforward-reference

ref doesn't have a value inside event handlers


Aimed functionality:
When a user clicks a button, a list shows. When he clicks outside the list, it closes and the button should receive focus. (following accessibility guidelines)

What I tried:

  const hideList = () => {
    // This closes the list
    setListHidden(true);
    // This takes a ref, which is forwarded to <Button/>, and focuses it
    button.current.focus();
  }

  <Button
    ref={button}
  />

Problem:
When I examined the scope of hideList function, found that ref gets the proper reference to button every where but inside the click event handler, it's {current: null}.
The console outputs: Cannot read property 'focus' of null

Example:
https://codepen.io/moaaz_bs/pen/zQjoLK
- click on the button and then click outside and review the console.


Solution

  • Since you are already using hooks in your App, the only change you need to make is to use useRef instead of createRef to generate a ref to the list.

    const Button = React.forwardRef((props, ref) => {
      return (
        <button 
          onClick={props.toggleList} 
          ref={ref}
        >
          button
        </button>
      );
    })
    
    const List = (props) => {
      const list = React.useRef();
    
      handleClick = (e) => {
        const clickIsOutsideList = !list.current.contains(e.target);
        console.log(list, clickIsOutsideList);
        if (clickIsOutsideList) {
          props.hideList();
        }
      }
    
      React.useEffect(function addClickHandler() {
        document.addEventListener('click', handleClick);
      }, []);
    
      return (
        <ul ref={list}>
          <li>item</li>
          <li>item</li>
          <li>item</li>
        </ul>
      );
    }
    
    const App = () => {
      const [ListHidden, setListHidden] = React.useState(true);
    
      const button = React.useRef();
    
      const toggleList = () => {
        setListHidden(!ListHidden);
      }
    
      const hideList = () => {
        setListHidden(true);
        button.current.focus();
      }
    
      return (
        <div className="App">
          <Button 
            toggleList={toggleList} 
            ref={button}
          />
          {
            !ListHidden &&
            <List hideList={hideList} />
          }
        </div>
      );
    }
    
    ReactDOM.render(<App />, document.getElementById('root'));
    

    Working demo

    The reason that you need it is because on every render of your Functional component, a new ref will be generated if you make use of React.createRef whereas useRef is implemented such that it generates a ref when its called the first time and returns the same reference anytime in future re-renders.

    P.S. A a thumb rule, you can say that useRef should be used when you want to have refs within functional components whereas createRef should be used within class components.