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.
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 whereascreateRef
should be used within class components.
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'));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With