Running on Mount
Video Summary
In this video, we explore how we might build an “auto-focusing” text input, one that captures focus when the component mounts.
Suppose we have the following code:
function App() { const [ searchTerm, setSearchTerm, ] = React.useState('');
return ( <main> <form> {/* I want to focus this input on mount: */} <input value={searchTerm} onChange={(event) => { setSearchTerm(event.target.value); }} /> <button>Search</button> </form> </main> );}
We can capture a reference to that input with the useRef
hook, as we saw earlier:
function App() { const [ searchTerm, setSearchTerm, ] = React.useState('');
const inputRef = React.useRef();
return ( <main> <form> <input ref={inputRef} value={searchTerm} onChange={(event) => { setSearchTerm(event.target.value); }} /> <button>Search</button> </form> </main> );}
Input DOM nodes have a .focus()
method we can call to focus it, but how do I do it on mount?
I can try to do it right in the render:
function App() { const [ searchTerm, setSearchTerm, ] = React.useState('');
const inputRef = React.useRef(); inputRef.current.focus();
// ✂️}
Unfortunately, this leads to an error:
The trouble is that when we first create the inputRef
, it's empty ({ current: undefined }
). It only captures the input DOM node after the first render.
The solution is to use the useEffect
hook:
function App() { const [ searchTerm, setSearchTerm, ] = React.useState('');
const inputRef = React.useRef();
React.useEffect(() => { inputRef.current.focus(); }, []);
// ✂️}
Critically, we're passing an empty dependency array. This is how we tell React, “this effect doesn't depend on any other values”. And if it doesn't depend on any values, it'll never re-run!
Effects always run after the first render, and then again whenever the dependencies change. This structure ensures it'll only run after the first render.
Here's the sandbox from the video. Uncomment the line in the effect to pull your focus:
Code Playground
Subscriptions
Let's suppose we want to track the user's cursor position. Whenever they move their mouse, we'll update some state.
We can add onMouseMove
event handlers to specific DOM nodes, like this:
<div onMouseMove={event => { setMousePosition({ x: event.clientX, y: event.clientY, }); }}>
This will only work while the user is hovering over this particular <div>
, though… What if we want to track their cursor position no matter where the mouse is within the viewport?
Spend a few moments tinkering, if you'd like, to see if you can come up with a solution. A sandbox has been provided:
Code Playground
Let's talk through it:
Video Summary
To listen for global mouse-move events, we need to use window.addEventListener
. We can register it in an effect hook, like this:
React.useEffect(() => { function handleMouseMove(event) { setMousePosition({ x: event.clientX, y: event.clientY, }); }
window.addEventListener('mousemove', handleMouseMove);});
At first glance, this seems to work, but there are two problems with this approach.
First, we aren't ever cleaning up our event listener. Let's put that one on the back burner for now; we'll talk about cleanup in the next lesson.
The issue I want to talk about is that we're adding multiple event listeners.
Because we haven't specified a dependency array, this effect will run after every single render. That means every time the user's mouse position changes, we call window.addEventListener
again. If 100 mouse-move events fire, we'll have 100 event listeners.
window.addEventListener
is a subscription. We only want to subscribe once, when the component first mounts.
Here's what the proper solution looks like:
React.useEffect(() => { function handleMouseMove(event) { setMousePosition({ x: event.clientX, y: event.clientY, }); }
window.addEventListener('mousemove', handleMouseMove);}, []);
window.addEventListener
is not part of React, it's part of the DOM. When we call this method, we set up a long-running process that will call our callback function whenever the mousemove
event is detected.
It's the same story with many other situations, like:
- Running an interval
- Opening a web socket connection
- Using
ResizeObserver
I have some illustrations that show visually what's going on here.
With an empty dependency array, the effect only runs once, after the first render, starting a single long-running process:
Without the empty dependency array, however, our effect runs after every render, starting multiple long-running processes:
Here are the diagrams from the video:
With an empty dependency array:
With no dependency array:
And here's the final solution from the video:
Code Playground