ref doesn't have a value inside event handlers
Asked Answered
S

2

5

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.

Shizukoshizuoka answered 25/5, 2019 at 15:24 Comment(0)
R
8

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.

Redingote answered 29/5, 2019 at 5:16 Comment(2)
Glad to have helped :-)Redingote
Thank you @Shubham Khatri, you saved my 7 hours of headache.Gradely
U
1

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'));
Ultrared answered 25/5, 2019 at 15:57 Comment(3)
I'm trying to forwardRef, but it weirdly doesn't pass, but I know the basics of using refs. Thank you for trying to helpShizukoshizuoka
Ooh my bad. ok no problem so use const button = React.createRef(); outside your App components. editing ans with updated forwardRefUltrared
Your solution works, but can you please tell me why can't I declare it inside the App component? Thank you so much for your time ..Shizukoshizuoka

© 2022 - 2024 — McMap. All rights reserved.