Custom Hooks Reuse Stateful Logic, Not State Itself
Displaying mouse coordinates on a React page isn't complicated: prepare two state variables, listen for mousemove, and render the coordinates.
The most straightforward approach might look like this:
import { useEffect, useState } from 'react';
function App() {
const [x, setX] = useState(null);
const [y, setY] = useState(null);
useEffect(() => {
function handleMouseMove(event) {
setX(event.clientX);
setY(event.clientY);
}
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return (
<div>
x: {x},y: {y}
</div>
);
}
This code works, but the component takes on three responsibilities at once:
- Managing mouse coordinate state.
- Subscribing to and cleaning up browser events.
- Deciding how the page displays the coordinates.
If another component also needs the mouse position, you'd have to copy this useState + useEffect block.
What's truly worth reusing isn't the <div> on the page, but the logic with its lifecycle that "converts browser mouse events into React state."
That's exactly the problem custom Hooks are meant to solve.
1. Custom Hooks reuse stateful logic, not code snippets
A plain utility function can encapsulate a calculation:
function add(a, b) {
return a + b;
}
But it can't directly call useState, useEffect, or other React Hooks inside itself.
A custom Hook is still essentially a JavaScript function, except it follows React Hooks' rules of invocation and can compose other Hooks:
function useSomething() {
const [state, setState] = useState();
useEffect(() => {
// Subscribe to an external system
}, []);
return state;
}
By convention, a custom Hook's name must start with use. This naming isn't for aesthetics; it tells React, ESLint, and other developers that this function may call Hooks internally and must obey the Rules of Hooks.
The two most important rules are:
- Only call Hooks inside React function components or custom Hooks.
- Call them at the top level of your function — never inside conditions, loops, or regular event handlers.
2. Extract mouse state and event listening into useMouse
You can move the logic originally written inside App into src/hooks/useMouse.js:
import { useEffect, useState } from 'react';
export function useMouse() {
const [position, setPosition] = useState({
x: null,
y: null,
});
useEffect(() => {
function handleMouseMove(event) {
setPosition({
x: event.clientX,
y: event.clientY,
});
}
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
return position;
}
This Hook only exposes the result the component truly needs:
{
x,
y
}
How the coordinates are produced, where the event is listened to, and how cleanup happens when the component unmounts — all of that is encapsulated inside useMouse.
Business components don't need to understand these implementation details.
3. The component only handles display
After extraction, App becomes very simple:
import { useMouse } from './hooks/useMouse.js';
function App() {
const { x, y } = useMouse();
const hasMoved = x !== null && y !== null;
return (
<div
style={{
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{hasMoved ? `x: ${x},y: ${y}` : 'Mouse has not moved yet'}
</div>
);
}
export default App;
Now the boundaries of responsibility are clear:
useMouseis responsible for converting browser events into React state.Appis responsible for deciding how that state is displayed.
In the future, whether it's a coordinate panel, a drag component, or a tooltip that follows the mouse, they can all call the same useMouse.
4. Why can't you write x && y?
In the original demo you might see a check like this:
{x && y ? `x: ${x}, y: ${y}` : 'Mouse has not moved'}
This code has a subtle edge-case problem.
When the mouse moves to the far left of the browser, x can be 0; when it moves to the very top, y can also be 0. But JavaScript treats the number 0 as falsy, so the page incorrectly displays "Mouse has not moved."
What you really need to check isn't whether the coordinates are truthy, but whether they are still the initial value null:
const hasMoved = x !== null && y !== null;
This detail shows that conditional rendering should be written around business semantics, not casually relying on JavaScript's truthy/falsy coercion.
5. Why must useEffect return a cleanup function?
document.addEventListener registers an event listener on a browser object outside the React component.
When the component unmounts, it won't automatically execute:
document.removeEventListener(...);
for us.
Without cleanup, old listener functions may persist, causing redundant executions, duplicate subscriptions, or memory leaks.
Therefore, an Effect that creates a subscription should also describe how to undo that subscription:
useEffect(() => {
function handleMouseMove(event) {
// handle event
}
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, []);
You must use the same handleMouseMove function reference here. The following approach cannot remove the original listener:
document.addEventListener('mousemove', () => {
// ...
});
document.removeEventListener('mousemove', () => {
// this is a different, new function
});
Even if the code inside two arrow functions is identical, they are not the same function object.
Why might you see an Effect run twice in development?
If the entry point uses StrictMode, React may execute a "mount, cleanup, remount" check flow in development.
This isn't the [] dependency array failing, nor is React creating two real component instances; it's helping us discover side effects that aren't properly cleaned up.
As long as the Effect's subscription and cleanup remain symmetric:
add listener → remove listener → add listener again
the final result will only retain one valid subscription.
So rather than saying useEffect(..., []) "absolutely only runs once," a more accurate description is: it describes the subscription logic for one mount cycle of the component; under strict mode checks in development, React may perform extra setup and teardown to verify the code is safe.
6. Does calling the same custom Hook share the same state?
This is one of the most common misconceptions about custom Hooks.
Suppose two components both call useMouse:
function Header() {
const position = useMouse();
}
function Panel() {
const position = useMouse();
}
They reuse the same logic, but they do not automatically share the same position state.
Each call to a custom Hook creates a set of Hook state and Effects belonging to the current component. The example above would register two mousemove listeners and maintain two separate coordinate states.
So you need to distinguish between two concepts:
- Custom Hook: reuses stateful logic.
- Context: shares the same piece of data across multiple components.
If many components need the exact same mouse coordinates and the page is performance-sensitive, you can call useMouse only once at a higher level and then pass the result down via Props or Context, rather than having every component register a global listener repeatedly.
7. When is it worth extracting a custom Hook?
You don't have to extract a Hook just because the code gets longer. A more practical criterion is: does a section of stateful logic appear in the component that can be independently named?
For example:
- State and an Effect always appear together as a group.
- Multiple components need to repeat the same subscription logic.
- The component mixes data fetching, browser APIs, and UI rendering to the point of being hard to read.
- This logic can be described with a clear name, such as
useMouse,useOnlineStatus,useTodos.
A custom Hook also doesn't mean stuffing all code into a hooks directory. A function that only performs plain string transformations, array sorting, or mathematical calculations should usually remain a regular utility function.
You only need a custom Hook when it needs to call other React Hooks or express logic related to the component lifecycle.
8. mousemove fires at high frequency — does it need optimization?
mousemove fires very frequently, and each call to setPosition can trigger a component update.
In this teaching demo, the component structure is simple, and updating state directly is enough to illustrate how the Hook works; there's no need for premature optimization.
But if mouse coordinates are driving a complex page, charts, or a large DOM, you can further consider:
- Using
requestAnimationFrameto batch multiple updates within a single frame. - Throttling the event.
- Only mounting the component when listening is truly needed.
- Avoiding having a large number of components each call
useMouseindividually.
Optimization should be based on actual performance problems, not mechanically adding throttling code just because you see a high-frequency event.
9. Summary
The value of a custom Hook isn't writing fewer lines of code, but establishing a clear boundary around a piece of stateful, lifecycle-aware logic.
In this mouse coordinate demo:
useStatestores the latest coordinates.useEffectconnects React to the browser'smousemoveevent.- The cleanup function removes the listener when the component unmounts.
useMousereturns the coordinates externally; the component is only responsible for rendering.
At the same time, remember: calling the same custom Hook only reuses logic; it does not make multiple components automatically share the same state.
In one sentence: Plain functions reuse computation, custom Hooks reuse state and side effects, and Context is what lets multiple components share the same data.