I think for removeEventListener to work in this case, in addition to passing the reference equal move for it, the functions might also need to be saved by useCallback. Because when state position changes and the component re-renders, handleMouseUp could be recreated with a new copy of move already.
Example with reference equal move and useCallback:
const Child = ({ image, text, backgroundRef }) => {
const [position, setPosition] = React.useState(null);
const move = React.useCallback((e) => {
setPosition({
top: `${e.clientY - 50}px`,
left: `${e.clientX - 65}px`,
});
}, []);
const handleMouseDown = React.useCallback(() => {
backgroundRef.current.addEventListener('mousemove', move);
}, [move]);
const handleMouseUp = React.useCallback(() => {
backgroundRef.current.removeEventListener('mousemove', move);
}, [move]);
return (
<div
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
style={position}
className="icon-container"
>
<img draggable="false" src={image} alt="" />
<p>{text}</p>
</div>
);
};
const Parent = () => {
const backgroundRef = React.useRef(null);
return (
<section ref={backgroundRef}>
<Child
text="👉 Drag this"
image="https://picsum.photos/100"
backgroundRef={backgroundRef}
/>
</section>
);
};
const App = () => {
return (
<div>
<Parent />
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
section {
position: relative;
width: 200px;
height: 200px;
background-color: pink;
display: flex;
justify-content: center;
align-items: center;
}
.icon-container {
position: absolute;
background-color: lightgreen;
cursor: pointer;
}
.icon-container > * {
pointer-events: none;
user-select: none;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
But alternatively, if the goal is to make the component follow the mouse move, it might not be necessary to host the event from the parent. The component could keep a active state to be toggled by clicks, which controls whether the dragging is active.
useCallback, addEventListener, and a ref from the parent can be omitted with this optional approach.
Example with active state:
const Child = ({ image, text }) => {
const [active, setActive] = React.useState(false);
const [position, setPosition] = React.useState(null);
const move = (e) => {
if (!active) return;
setPosition({
top: `${e.clientY - 50}px`,
left: `${e.clientX - 65}px`,
});
};
return (
<div
onMouseDown={() => setActive(true)}
onMouseUp={() => setActive(false)}
style={position}
className="icon-container"
onMouseMove={active ? move : null}
>
<img draggable="false" src={image} alt="" />
<p>{text}</p>
</div>
);
};
const Parent = () => {
return (
<section>
<Child text="👉 Drag this" image="https://picsum.photos/100" />
</section>
);
};
const App = () => {
return (
<div>
<Parent />
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
section {
position: relative;
width: 200px;
height: 200px;
background-color: lightblue;
display: flex;
justify-content: center;
align-items: center;
}
.icon-container {
position: absolute;
background-color: pink;
cursor: pointer;
}
.icon-container > * {
pointer-events: none;
user-select: none;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
removeEventListeneryou have to pass the same function reference, creating a new function won't work.(e)=>move(e)creates a new function