Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 946
Moaaz Bhnas Avatar asked May 25 '19 15:05

Moaaz Bhnas


2 Answers

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.

like image 142
Shubham Khatri Avatar answered Nov 17 '22 03:11

Shubham Khatri


Create your ref

this.button = React.createRef();

Add Ref to your DOM element

ref={this.button}

Use the Ref as per requirement

this.button.current.focus();

Complete code using forwarding-refs

const Button = React.forwardRef((props, ref) => {
    return (
        <button
            onClick={props.toggleList}
            ref={ref}
        >
            button
        </button>
    );
})

const List = (props) => {
    const list = React.createRef();

    handleClick = (e) => {
        const clickIsOutsideList = !list.current.contains(e.target);
        if (clickIsOutsideList) {
            props.hideList();
        }
    }

    React.useEffect(function addClickHandler() {
        document.addEventListener('click', handleClick);
        return function clearClickHandler() {
            document.removeEventListener('click', handleClick);
        }
    }, []);

    return (
        <ul ref={list}>
            <li>item</li>
            <li>item</li>
            <li>item</li>
        </ul>
    );
}

const button = React.createRef();

const App = () => {
    const [ListHidden, setListHidden] = React.useState(true);
    const toggleList = () => {
        setListHidden(!ListHidden);
    }

    const hideList = () => {
        setListHidden(true);
        console.log(button)
        button.current.focus();
    }

    return (
        <div className="App">
            <Button
                toggleList={toggleList}
                ref={button}
            />
            {
                !ListHidden &&
                <List hideList={hideList} />
            }
        </div>
    );
}

ReactDOM.render(<App />, document.getElementById('root'));
like image 32
Himanshu Pandey Avatar answered Nov 17 '22 02:11

Himanshu Pandey