Reputation: 31475
Here's my situation:
I've got a custom hook, called useClick
, which gets an HTML element
and a callback
as input, attaches a click
event listener to that element
, and sets the callback
as the event handler.
App.js
function App() {
const buttonRef = useRef(null);
const [myState, setMyState] = useState(0);
function handleClick() {
if (myState === 3) {
console.log("I will only count until 3...");
return;
}
setMyState(prevState => prevState + 1);
}
useClick(buttonRef, handleClick);
return (
<div>
<button ref={buttonRef}>Update counter</button>
{"Counter value is: " + myState}
</div>
);
}
useClick.js
import { useEffect } from "react";
function useClick(element, callback) {
console.log("Inside useClick...");
useEffect(() => {
console.log("Inside useClick useEffect...");
const button = element.current;
if (button !== null) {
console.log("Attaching event handler...");
button.addEventListener("click", callback);
}
return () => {
if (button !== null) {
console.log("Removing event handler...");
button.removeEventListener("click", callback);
}
};
}, [element, callback]);
}
export default useClick;
Note that with the code above, I'll be adding and removing the event listener on every call of this hook (because the callback, which is handleClick
changes on every render). And it must change, because it depends on the myState
variable, that changes on every render.
And I would very much like to only add the event listener on mount and remove on dismount. Instead of adding and removing on every call.
Here on SO, someone have suggested that I coulde use the following:
useClick.js
function useClick(element, callback) {
console.log('Inside useClick...');
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const callbackWrapper = useCallback(props => callbackRef.current(props), []);
useEffect(() => {
console.log('Inside useClick useEffect...');
const button = element.current;
if (button !== null) {
console.log('Attaching event handler...');
button.addEventListener('click', callbackWrapper);
}
return () => {
if (button !== null) {
console.log('Removing event handler...');
button.removeEventListener('click', callbackWrapper);
}
};
}, [element, callbackWrapper]);
}
QUESTION
It works as intended. It only adds the event listener on mount, and removes it on dismount.
The code above uses a callback wrapper that uses a ref that will remain the same across renders (so I can use it as the event handler and mount it only once), and its .current
property it's updated with the new callback on every render by a useEffect
hook.
The question is: performance-wise, which approach is the best? Is running a useEffect()
hook less expensive than adding and removing event listeners on every render?
Is there anyway I could test this?
Upvotes: 6
Views: 1228
Reputation: 320
App.js
function App() {
const buttonRef = useRef(null);
const [myState, setMyState] = useState(0);
// handleClick remains unchanged
const handleClick = useCallback(
() => setMyState(prevState => prevState >= 3 ? 3 : prevState + 1),
[]
);
useClick(buttonRef, handleClick);
return (
<div>
<button ref={buttonRef}>Update counter</button>
{"Counter value is: " + myState}
</div>
);
}
A more professional answer:
App.js
function App() {
const buttonRef = useRef(null);
const [myState, handleClick] = useReducer(
prevState => prevState >= 3 ? 3 : prevState + 1,
0
);
useClick(buttonRef, handleClick);
return (
<div>
<button ref={buttonRef}>Update counter</button>
{"Counter value is: " + myState}
</div>
);
}
Upvotes: 1