I am attempting to add and remove an event listener within a functional React component. The listener is added fine but is not removed when asked to be. I believe the issue is that the function I am referencing handlemousemove is recreated every component render and so when removeEventListener attempts to remove it, it's not the same function reference as when addEventListener added it.
I tried moving handlemousemove out of the component but it required access to the setState hooks generated in the component.
const handleMouseMove = e => {
setYOffset(e.clientY-280)
setXOffset(e.clientX-350)
}
const followMouse = () => {
if (isFollowingMouse){
setIsFollowingMouse(false)
document.removeEventListener("mousemove", handleMouseMove)
} else {
setIsFollowingMouse(true)
document.addEventListener("mousemove", handleMouseMove)
}
}
...
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
All branches of execution are hit here but document.removeEventListener("mousemove", handleMouseMove) doesn't actually remove the event listener.
Is there a way to have a "static method" within a functional component? Is that even the issue here?
Here's a link to code sandbox with the whole code: https://codesandbox.io/s/pzrwh
The old way to do it was with render props, but now that hooks have arrived this is a better solution
const MyComponent = (props) => {
const [isFollowingMouse, setIsFollowingMouse] = React.useState(false);
const [xOffset, setXOffset] = React.useState(0);
const [yOffset, setYOffset] = React.useState(0);
const handleMouseMove = e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
};
const followMouse = () => {
setIsFollowingMouse(!isFollowingMouse);
}
const styles = {
'cat': {
'backgroundColor': 'red',
'height': '20px',
'position': 'absolute',
'left': xOffset,
'top': yOffset,
'width': '20px',
'display': isFollowingMouse ? 'block' : 'none'
}
};
return (
<div style={{ 'height': '100%' }} onMouseMove={handleMouseMove}>
<div style={ styles.cat }>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
html,
body,
#root {
height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I think your description of the issue is spot on. A quick fix is to define the variable handleMouseMove outside of your App function - essentially making the variable static and not recreated every render.
Then, within the body of the function, only assign the handleMouseMove variable if it's currently unassigned, and set it back to null when you set isFollowingMouse to false.
With React 16.7 you can use Hooks to do this:
import React, { useCallback, useEffect, useState } from 'react';
const DraggedComponent = React.memo(
props => {
const [isFollowingMouse, setIsFollowingMouse] = useState(false);
const [xOffset, setXOffset] = useState(0);
const [yOffset, setYOffset] = useState(0);
const handleMouseMove = useCallback(
e => {
if (isFollowingMouse) {
setYOffset(e.clientY-28);
setXOffset(e.clientX-35);
}
}, [isFollowingMouse, setYOffset, setXOffset]
);
useEffect(
() => {
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
},
[handleKeyDown]
);
const followMouse = () => setIsFollowingMouse(!isFollowingMouse);
return (
<div onMouseMove={handleMouseMove}>
<div>C</div>
<button name="mouse" onClick={followMouse}>
Follow Mouse
</button>
</div>
)
}
);
ReactDOM.render(<DraggedComponent />, document.getElementById('root'));
In this example React.memo() ensures that the component is only redrawn if state or properties change. Similar useCallback() will cache the event listener for the mousemove event, such that this will not be recreated only if isFollowingMouse, setYOffset or setXOffset change, instead of every rerender. useEffect will be called once the component is created, and once every time the handleMouseMove callback changes. Furthermore it returns a function, which is automatically called if the component is destroyed or the parameter handleKeyDown changes.
Related
I want to calculate the height of a component and send it to its parent when the page is loaded and resized.
I'm using the below reusable Hook to successfully measure the height of the div inside the header component. But how do I send the height calculated from useDimensions in the child to its parent component as headHeight?
Measuring Hook
import { useState, useCallback, useEffect } from 'react';
function getDimensionObject(node) {
const rect = node.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
top: 'x' in rect ? rect.x : rect.top,
left: 'y' in rect ? rect.y : rect.left,
x: 'x' in rect ? rect.x : rect.left,
y: 'y' in rect ? rect.y : rect.top,
right: rect.right,
bottom: rect.bottom
};
}
export function useDimensions(data = null, liveMeasure = true) {
const [dimensions, setDimensions] = useState({});
const [node, setNode] = useState(null);
const ref = useCallback(node => {
setNode(node);
}, []);
useEffect(() => {
if (node) {
const measure = () =>
window.requestAnimationFrame(() =>
setDimensions(getDimensionObject(node))
);
measure();
if (liveMeasure) {
window.addEventListener('resize', measure);
window.addEventListener('scroll', measure);
return () => {
window.removeEventListener('resize', measure);
window.removeEventListener('scroll', measure);
};
}
}
}, [node, data]);
return [ref, dimensions, node];
}
Parent
export default function Main(props: { notifications: Notification[] }) {
const { notifications } = props;
const [headHeight, setHeadHeight] = useState(0)
const updateHeadHeight = () => {
setHeadHeight(headHeight)
}
return (
<main>
<Header updateParent={updateHeadHeight}/>
{headHeight}
</main>
)
}
Child
import { useDimensions } from '../../lib/design/measure';
import React, { useState, useLayoutEffect } from 'react';
export default function DefaultHeader(props, data) {
const [
ref,
{ height, width, top, left, x, y, right, bottom }
] = useDimensions(data);
;
return <>
<div ref={ref} className="header">
<h1>Hello!</h1>
</div>
</>
}
Personally, I would call the hook in the Main component and wrap the child component in a forwardRef (check docs here).
See full example here:
Main.tsx
export default function Main(props: { notifications: Notification[] }) {
const { notifications } = props;
const [ref, dimensions] = useDimensions()
return (
<main>
<Header ref={ref}/>
{JSON.stringify(dimensions)}
</main>
)
}
What's done here, we just pass the ref down the tree to the child component and we just show the dimensions (testing purposes).
DefaultHeader.tsx
import { forwardRef } from "react";
const DefaultHeader = forwardRef((_, ref) => {
return (
<>
<div ref={ref} className="header" >
<h1>Hello!</h1>
</div>
</>
);
});
export default DefaultHeader;
Here, we just attach the ref to the container that it has previously been (your example).
See full example on this CodeSandbox.
Let me know if you need more explanations on this.
You could just add a useEffect hook within the DefaultHeader component like this:
useEffect(() => props.updateParent(height), [props.updateParent, height])
This hook should run anytime it detects changes to the height variable or props.updateParent props. Just make sure you are declaring this hook after the useDimensions hook so it doesn't throw an undefined error.
You are seriously overcomplicating this by limiting yourself to the react way of doing things. You can simply use Event Bubbling to achieve what you want. All you need to do is dispatch a Custom Event event on the child element that will bubble up through each ancestor to the window. Then intercept this event in the parent element and react to it.
let m = document.querySelector('main');
let s = document.querySelector('section');
let b = document.querySelector('button');
b.addEventListener('click', event => {
s.style.height = '200px';
});
m.addEventListener('resize', event => {
console.log('new size:', event.detail);
});
new ResizeObserver(entries => {
for (let e of entries) {
s.dispatchEvent(
new CustomEvent('resize', {
detail: e.contentRect,
bubbles: true
})
);
}
}).observe(s);
main { background: green; padding: 1rem; margin-top: 1rem; }
section { background: orange; }
<button> Test </button>
<main>
<section></section>
</main>
Just implement the ResizeObserver block in the child component and the resize Event Listener in the parent component.
This method achieves the desired effect in a decoupled and standards-compliant way. The child element is merely announcing the change to it's state (in the DOM, not React state). Any ancestor element can then act on this behavior as needed or ignore it. The parent element can also receive this resize event from any descendant element regardless of how deeply nested it may be. It also does not care which descendant element has changed. If you have multiple descendants that could change, this will trigger for any one of them.
Additionally, the resize event here is NOT tied to the window. ANY changes to the child element's size will trigger the event listener on the parent.
For context, I have a web app that displays an image in a React-Bootstrap Container component (Arena) that holds an image where users are to look and find specific characters.
Separately, I created a div component (CustomCursor) where the background is set to a magnifying glass SVG image.
The Arena component tracks mouse position through an OnMouseMove handler function (handleMouseMove) and passes those coordinates as props to the CustomCursor component.
Here is my Arena component code:
import { useState, useEffect } from 'react';
import { Container, Spinner } from 'react-bootstrap';
import CustomCursor from '../CustomCursor/CustomCursor';
import Choices from '../Choices/Choices';
import { getImageURL } from '../../helpers/storHelpers';
import './Arena.scss';
export default function Arena(props) {
const [arenaURL, setArenaURL] = useState('');
const [loaded, setLoaded] = useState(false);
const [clicked, setClicked] = useState(false);
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handleClick(e) {
setClicked(true);
}
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
useEffect(() => {
retreiveArena();
// FUNCTION DEFINITIONS
async function retreiveArena() {
const url = await getImageURL('maps', 'the-garden-of-earthly-delights.jpg');
setArenaURL(url);
setLoaded(true);
}
}, [])
return (
<Container as='main' fluid id='arena' className='d-flex flex-grow-1 justify-content-center align-items-center' onClick={handleClick}>
{!loaded &&
<Spinner animation="border" variant="danger" />
}
{loaded &&
<img src={arenaURL} alt='The Garden of Earthly Delights triptych' className='arena-image' onMouseMove={handleMouseMove} />
}
{clicked &&
<Choices x={x} y={y} />
}
<CustomCursor x={x} y={y} />
</Container>
)
}
Here is my CustomCursor code:
import './CustomCursor.scss';
export default function CustomCursor(props) {
const { x, y } = props;
return (
<div className='custom-cursor' style={{ left: `${x - 64}px`, top: `${y + 50}px` }} />
)
}
When I first created the OnMouseMove handler function I simply set the x and y state values by passing them into their respective state setter functions directly:
function handleMouseMove(e) {
setX(e.clientX);
setY(e.clientY);
}
However, I noticed this was slow and laggy and when I refactored this function to use setter functions instead it was much faster (what I wanted):
function handleMouseMove(e) {
setX(prevState => { return e.clientX });
setY(prevState => { return e.clientY });
}
Before:
After:
Why are using setter functions faster than passing in values directly?
This is interesting. First of all, we need to focus on reacts way of updating state. In the documentation of react https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous There you can see:
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
For example, this code may fail to update the counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
A pretty good article on this is written by Jan Hesters here:
https://medium.com/#jan.hesters/updater-functions-in-reacts-setstate-63c7c162b16a
And more details here:
https://learn.co/lessons/react-updating-state
I am trying to apply a parallax effect to an .svg image by using useRef() to grab bubblesRef and translateY() onScroll.
The parallax works but when I navigate to the next page I receive error "TypeError: Cannot read property 'style' of null". I think it is because the addEventListener is still listening and trying to useRef() on bubblesRef while navigating to the next page. So I added the cleanup function in useEffect() but that doesn't seem to fix it.
Any help is appreciated. Thanks!
p.s. If anyone can share their approach to a simple parallax effect like this that would be great too. This is the only approach I've figured that won't rerender everything else on the page onScroll.
const HomePage = () => {
const [loadedPosts, setLoadedPosts] = useState([]);
const { sendRequest } = useHttpClient();
console.log("loadedPosts homePage", loadedPosts);
const bubblesRef = useRef();
useEffect(() => {
if (loadedPosts.length === 0) {
//api call
}
}, [sendRequest, loadedPosts]);
useEffect(() => {
const parallax = () => {
let scrolledValue = window.scrollY / 3.5;
bubblesRef.current.style.transform = `translateY(
-${scrolledValue + "px"}
)`;
console.log("scrolling...", scrolledValue);
};
window.addEventListener("scroll", parallax);
return () => window.removeEventListener("scroll", parallax);
}, []);
return (
<HomePageContainer>
<Header />
<SectionOne posts={loadedPosts} />
<SectionTwo />
<BubbleBlobs className="bubbleBlobs" ref={bubblesRef} />
<BlobTop className="backBlobBottom" preserveAspectRatio="none" />
</HomePageContainer>
);
};
export default HomePage;
You definitely need the cleanup function any time you add a listener to the window, or the handler (and thus the component instance itself) will live on forever. However, since React runs those cleanup hooks asynchronously, it might not happen until after other window events. The value of the ref is set to null when the component unmounts, so you need to check that it is still defined before using the value.
useEffect(() => {
const handler = () => {
if (ref.current) {
// perform update
}
}
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])
When you call useEffect, your reference has not been instantiated, so the error message appears, in your useEffect dependency array, insert your ref and before running the code in useEffect, make sure your current reference is defined.
I am currently writing a map component using Mapbox. But I encounter an error on React hooks during development.
In useEffect state variable that is declared prints two different values.
As i explain in the below code. startDrawing is console.logs both true and false after second click on <IconButton/> button.
import React from "react";
import mapboxgl from "mapbox-gl";
import { Add, Delete } from "#material-ui/icons";
import IconButton from "#material-ui/core/IconButton";
export default function MapComponent() {
const mapContainerRef = React.useRef(null);
const [startDrawing, setStartDrawing] = React.useState(false);
const [map, setMap] = React.useState(null);
const initMap = () => {
mapboxgl.accessToken = "mapbox-token";
const mapbox = new mapboxgl.Map({
container: mapContainerRef.current,
style: "mapbox://styles/mapbox/streets-v11",
center: [0, 0],
zoom: 12,
});
setMap(mapbox);
};
React.useEffect(() => {
if (!map) {
initMap();
} else {
map.on("click", function (e) {
// After second click on set drawing mode buttons
// startDrawing value writes two values for each map click
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
// MapComponent.js:85 true
// MapComponent.js:85 false
console.log(startDrawing);
if (startDrawing) {
// do stuff
} else {
// do stuff
}
});
}
}, [map, startDrawing]);
return (
<>
<div>
{/* set drawing mode */}
<IconButton onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <Delete /> : <Add />}
</IconButton>
</div>
<div ref={mapContainerRef} />
</>
);
}
So my question is how can i solve this problem?
Thank you for your answers.
The issue is that you are adding a new event listener to map every time startDrawing changes. When you click on the rendered element all of those listeners are going to be fired, meaning you get every state of startDrawing the component has seen.
See this slightly more generic example of your code, and note that every time you click Add or Delete a new event listener gets added to the target element:
const { useState, useRef, useEffect } = React;
function App() {
const targetEl = useRef(null);
const [startDrawing, setStartDrawing] = useState(false);
const [map, setMap] = useState(null);
const initMap = () => {
setMap(targetEl.current);
};
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
}
}, [map, startDrawing]);
return (
<div>
<div>
<button onClick={() => setStartDrawing(!startDrawing)}>
{startDrawing ? <span>Delete</span> : <span>Add</span>}
</button>
</div>
<div ref={targetEl}>target element</div>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can fix this by adding a return statement to your useEffect. This is triggered immediately before the effect updates with new values from the dependency array, and also when the component unmounts. Inside the return statement you should remove the previous event listener so that only one is attached to the element at any given point. For the above example it would look like this:
useEffect(() => {
if (!map) {
initMap();
} else {
const log = () => console.log(startDrawing);
map.addEventListener('click', log);
return () => {
map.removeEventListener('click', log);
};
};
}, [map, startDrawing]);
Ideally you would not use the standard JS event syntax at all, as the convention in React is to attach events declaratively in the return/render function so that they can always reference the current state. However, you are using an external library, and I don't know whether it has any explicit support for React - you should probably check that out.
The issue is that useEffect will trigger both on mount and unmount (render and destroy). Refer to this documentation for a detailed explanation.
To run the function only on the first render, you can pass an empty array as the second parameter of useEffect, just like this:
useEffect(()=>{
//do stuff
},[]); // <--- Look at this parameter
The last parameter serves as a flag and usually a state should be passed, which will make useEffect's function trigger only if the parameter's value is different from the previous.
Let's assume you want to trigger useEffect each and every time your state.map changes - you cold do the following:
const [map, setMap] = React.useState(null);
useEffect(()=>{
//do stuff
},map); // if map is not different from previous value, function won't trigger
I've been working with function components and hooks and i'm now trying to dig deeper into events and how they hold in memory. I've been using chrome dev tools performance tab to monitor the behavior. There is a few things I'm not clear on and maybe someone can clear this up for me.
So I did 3 different setups. First one to show obvious memory leak of events been added multiple times per render. which eventually causes a crash or infinite loop of renders. Or at least thats what looks like is happing.
const App = () => {
const [count, setCount] = React.useState(0);
const onKeyDown = () => setCount(count => count + 1);
document.addEventListener('keydown', onKeyDown);
return (
<div className='wrapper'>
<div>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
This shows an obvious spike in extra event calls per listener. See event log and then increase ladder of events been added.
Next up
const App = () => {
const [count, setCount] = React.useState(0);
const onKeyDown = () => setCount(count => count + 1);
React.useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [] );
return (
<div className='wrapper'>
<div>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
The result was better in that the listener was only one call at a time.
But I noticed the listener count was still going through the roof. The spike in when they got added wasn't as sharp. but the count of listeners was in the thousand. Where are all these listeners getting added. Is it listeners been added by jsfiddle. Probably best to isolate this test in just a html page outside jsfiddle.
Then I read about using the hook useCallback which memoizes the function and returns the cashed version of the function. So I tried this.
const App = () => {
const [count, setCount] = React.useState(0);
const cb = React.useCallback(() => {
console.log('cb');
setCount(count => count + 1);
}, [] );
return (
<div className='wrapper'>
<div onClick={cb}>Click any key to update counter</div>
<div className='counter'>{count}</div>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#app"))
But this turned out to be similar to the last test using useEffect.
Crazy amount of listeners still but no crashing like the first test.
So whats' the deal here am I missing something about memoizing using useCallback hook. Listeners look like they are been added like crazy and not been garbage collected.
I'm going to isolate this test without jsfiddle but just wanted to post to the community to get some insight on this first.
You don't use addEventListener in React!
Instead you'd do something like this:
const App = () => {
let count = 0;
const onAddHandler = () => {
count++;
console.log(count);
this._count.innerText = count;
}
return (
<div className='wrapper'>
<div onClick={()=>onAddHandler()}>Click any key to update counter</div>
<div className='counter' ref={(el) => this._count = el}></div>
</div>
);
}
Also, not sure why you're using React.useState. The whole point of functional components is that they're stateless. I'm not fond of this new useState hook to be used in a functional component.
The example you were probably looking for using hooks is:
import React, { useState, useEffect } from 'react';
import {render} from 'react-dom';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
render(<Example />, document.getElementById('root'));
The React documentation https://reactjs.org/docs/hooks-effect.html says that
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.