I am trying to make a hook that returns the clientX and clientY values when the mouse moves on the screen. My hook looks like this -
useMouseMove hook
const useMouseMove = () => {
const [mouseData, setMouseData] = useState<[number, number]>([0, 0])
useEffect(() => {
const handleMouse = (e: MouseEvent) => {
setMouseData([e.clientX, e.clientY])
}
document.addEventListener("mousemove", handleMouse)
return () => {
document.removeEventListener("mousemove", handleMouse)
}
}, [])
return mouseData
}
And I'm using it in another component like so,
Usage in component
const SomeComponent = () => {
const mouseData = useMouseMoveLocation()
console.log("Rendered") // I want this to be rendered only once
useEffect(() => {
// I need to use the mouseData values here
console.log({ mouseData })
}, [mouseData])
return <>{/* Some child components */}</>
}
I need to use the mouseData values from the useMouseMove hook in the parent component (named SomeComponent in the above example) without re-rendering the entire component every time the mouse moves across the screen. Is there a correct way to do this to optimise for performance?
If you're not going to be rendering this component, then you can't use a useEffect. useEffect's only get run if your component renders. I think you'll need to run whatever code you have in mind in the mousemove callback:
const useMouseMove = (onMouseMove) => {
useEffect(() => {
document.addEventListener("mousemove", onMouseMove)
return () => {
document.removeEventListener("mousemove", onMouseMove)
}
}, [onMouseMove])
}
const SomeComponent = () => {
useMouseMove(e => {
// do something with e.clientX and e.clientY
});
}
Hello I'm trying to pass the following code to reacthooks:
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
class SomeComponent extends React.Component {
// 2. Initialise your ref and targetElement here
targetRef = React.createRef();
targetElement = null;
componentDidMount() {
// 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
this.targetElement = this.targetRef.current;
}
showTargetElement = () => {
// ... some logic to show target element
// 4. Disable body scroll
disableBodyScroll(this.targetElement);
};
hideTargetElement = () => {
// ... some logic to hide target element
// 5. Re-enable body scroll
enableBodyScroll(this.targetElement);
}
componentWillUnmount() {
// 5. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
// OR useful for if the `hideTargetElement()` function got circumvented eg. visitor
// clicks a link which takes him/her to a different page within the app.
clearAllBodyScrollLocks();
}
render() {
return (
// 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
<SomeOtherComponent ref={this.targetRef}>
some JSX to go here
</SomeOtherComponent>
);
}
}
And then I did the following with hooks:
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
const targetRef = useRef();
const showTargetElement = () => {
disableBodyScroll(targetRef);
};
const hideTargetElement = () => {
enableBodyScroll(targetRef);
};
useEffect(() => {
if (modalIsOpen === true) {
showTargetElement();
} else {
hideTargetElement();
}
}, [modalIsOpen]);
I don't know if I did it correctly with useRef and useEffect, but it worked, but I can't imagine how I'm going to get to my componentWillUnmount to call mine:
clearAllBodyScrollLocks ();
The basic equivalents for componentDidMount and componentWillUnmount in React Hooks are:
//componentDidMount
useEffect(() => {
doSomethingOnMount();
}, [])
//componentWillUnmount
useEffect(() => {
return () => {
doSomethingOnUnmount();
}
}, [])
These can also be combined into one useEffect:
useEffect(() => {
doSomethingOnMount();
return () => {
doSomethingOnUnmount();
}
}, [])
This process is called effect clean up, you can read more from the documentation.
I'm currently understanding the useRef hook and its usage. Accessing the DOM is a pretty straight forward use case which I understood. The second use case is that a ref behaves like an instance field in class components. And the react docs provide an example of setting and clearing a time interval from a click handler. I want to know, if cancelling the time interval from a click handler is not required, can we set and clear intervals with local variables declared within useEffect like below? Or is using a ref as mentioned in the docs always the approach to be taken?
useEffect(() => {
const id = setInterval(() => {
// ...
});
return () => {
clearInterval(id);
};
})
As stated at the docs you shared;
If we just wanted to set an interval, we wouldn’t need the ref (id could be local to the effect).
useEffect(() => {
const id = setInterval(() => {
setCounter(prev => prev + 1);
}, 1000);
return () => {
clearInterval(id);
};
});
but it’s useful if we want to clear the interval from an event handler:
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
I think the example is just for demonstrating how useRef works, though I personal cannot find many use case for useRef except in <input ref={inputEl} /> where inputEl is defined with useRef. For if you want to define something like an component instance variable, why not use useState or useMemo? I want to learn that too actually (Why using useRef in this react example? just for concept demostration?)
As for the react doc example https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
I tried and can achieve the same without useRef as below:
function Timer() {
const interval = null;
useEffect(() => {
const id = setInterval(() => {
// ...
});
interval = id;
return () => {
clearInterval(interval);
};
});
// ...
function handleCancelClick() {
clearInterval(interval);
}
// ...
}
With old class based React.js code, I can do like this, to remember that a component has been unmounted:
componentWillUnmount: function() {
this.isGone = true;
},
loadUsers: function() {
Server.loadUsers(..., (response) => {
if (this.isGone) return;
...
});
}
How can one do the same thing, with hooks based components?
Here's a sample hooks function component, where I'm unsure about how to remember that it's been unmounted, so I can return before calling any setSomeState:
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
let isGone = false; // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (/* is unmounted ?? */) return;
setMembers(response.members);
});
return () => { /* how remember is unmounted?
would isGone = true; work? */ };
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (/* is unmounted ?? */) return;
setMembers([]);
});
}});
I suppose I cannot use const [isGone, setGone] = useState(false) because I shouldn't try to access the state (read isGone) after has been unmounted. And if I [222] add a local let isGone = false inside the function, it seems to me that various callbacks created inside the function, will refer to different "instances" of this local variable, depending on in which different GroupMembers(..) invokation the different callbacks were created? or am I mistaken and this works? — Maybe I could create an outer wrapper function with a local let isGone = false, however, this adds another wrapping function and indentation :- /
Since you are using the isGone flag outside of the useEffect method too, you can make use of useRef to store variables like
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
const isGone = useRef(false); // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (isGone.current) return;
setMembers(response.members);
});
return () => {
isGone.current = true;
};
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (isGone.current) return;
setMembers([]);
});
}});
PS: A better way to handle such things is to cancel the request when
you are leaving the page instead of waiting for the response only to
neglect it.
just use local variable accessed through closure
React.useEffect(() => {
let isActual = true;
Server.listGroupMembers(someGroupId, (response) => {
if (!isActual) return;
setMembers(response.members);
});
return () => {isActual = false;};
}, []);
in this case flag would be updated on unmounting only. but in general case(with some unempty dependencies for useEffect) it will use as well. So in case of sequential renderings you could be sure you never process older request.
PS most universal way is cancelling request when it's possible.
I'm trying to add debouncing with lodash to a search function, called from an input onChange event. The code below generates a type error 'function is expected', which I understand because lodash is expecting a function. What is the right way to do this and can it be done all inline? I have tried nearly every example thus far on SO to no avail.
search(e){
let str = e.target.value;
debounce(this.props.relay.setVariables({ query: str }), 500);
},
With a functional react component try using useCallback. useCallback memoizes your debounce function so it doesn't get recreated again and again when the component rerenders. Without useCallback the debounce function will not sync with the next key stroke.
`
import {useCallback} from 'react';
import _debounce from 'lodash/debounce';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useCallback(_debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
`
The debounce function can be passed inline in the JSX or set directly as a class method as seen here:
search: _.debounce(function(e) {
console.log('Debounced Event:', e);
}, 1000)
Fiddle: https://jsfiddle.net/woodenconsulting/69z2wepo/36453/
If you're using es2015+ you can define your debounce method directly, in your constructor or in a lifecycle method like componentWillMount.
Examples:
class DebounceSamples extends React.Component {
constructor(props) {
super(props);
// Method defined in constructor, alternatively could be in another lifecycle method
// like componentWillMount
this.search = _.debounce(e => {
console.log('Debounced Event:', e);
}, 1000);
}
// Define the method directly in your class
search = _.debounce((e) => {
console.log('Debounced Event:', e);
}, 1000)
}
This is how I had to do it after googling the whole day.
const MyComponent = (props) => {
const [reload, setReload] = useState(false);
useEffect(() => {
if(reload) { /* Call API here */ }
}, [reload]);
const callApi = () => { setReload(true) }; // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
function handleChange() {
debouncedCallApi();
}
return (<>
<input onChange={handleChange} />
</>);
}
That's not so easy question
On one hand to just work around error you are getting, you need to wrap up you setVariables in the function:
search(e){
let str = e.target.value;
_.debounce(() => this.props.relay.setVariables({ query: str }), 500);
}
On another hand, I belive debouncing logic has to be incapsulated inside Relay.
A lot of the answers here I found to be overly complicated or just inaccurate (i.e. not actually debouncing). Here's a straightforward solution with a check:
const [count, setCount] = useState(0); // simple check debounce is working
const handleChangeWithDebounce = _.debounce(async (e) => {
if (e.target.value && e.target.value.length > 4) {
// TODO: make API call here
setCount(count + 1);
console.log('the current count:', count)
}
}, 1000);
<input onChange={handleChangeWithDebounce}></input>
Improving on this answer: https://stackoverflow.com/a/67941248/2390312
Using useCallback and debounce is known to cause an eslint exhaustive deps warning.
Here's how to do it with functional components and useMemo
import { useMemo } from 'react';
import { debounce } from 'lodash';
import axios from 'axios';
function Input() {
const [value, setValue] = useState('');
const debounceFn = useMemo(() => debounce(handleDebounceFn, 1000), []);
function handleDebounceFn(inputValue) {
axios.post('/endpoint', {
value: inputValue,
}).then((res) => {
console.log(res.data);
});
}
function handleChange (event) {
setValue(event.target.value);
debounceFn(event.target.value);
};
return <input value={value} onChange={handleChange} />
}
We are using useMemo to return a memoized value, where this value is the function returned by debounce
Some answers are neglecting that if you want to use something like e.target.value from the event object (e), the original event values will be null when you pass it through your debounce function.
See this error message:
Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property nativeEvent on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().
As the message says, you have to include e.persist() in your event function. For example:
const onScroll={(e) => {
debounceFn(e);
e.persist();
}}
Then of course, your debounceFn needs to be scoped outside of the return statement in order to utilize React.useCallback(), which is necessary. My debounceFn looks like this:
const debounceFn = React.useCallback(
_.debounce((e) =>
calculatePagination(e),
500, {
trailing: true,
}
),
[]
);
#Aximili
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000));
looks strange :) I prefare solutions with useCallback:
const [searchFor, setSearchFor] = useState('');
const changeSearchFor = debounce(setSearchFor, 1000);
const handleChange = useCallback(changeSearchFor, []);
for your case, it should be:
search = _.debounce((e){
let str = e.target.value;
this.props.relay.setVariables({ query: str });
}, 500),
class MyComp extends Component {
debounceSave;
constructor(props) {
super(props);
}
this.debounceSave = debounce(this.save.bind(this), 2000, { leading: false, trailing: true });
}
save() is the function to be called
debounceSave() is the function you actually call (multiple times).
This worked for me:
handleChange(event) {
event.persist();
const handleChangeDebounce = _.debounce((e) => {
if (e.target.value) {
// do something
}
}, 1000);
handleChangeDebounce(event);
}
This is the correct FC approach
#
Aximili answers triggers only one time
import { SyntheticEvent } from "react"
export type WithOnChange<T = string> = {
onChange: (value: T) => void
}
export type WithValue<T = string> = {
value: T
}
// WithValue & WithOnChange
export type VandC<A = string> = WithValue<A> & WithOnChange<A>
export const inputValue = (e: SyntheticEvent<HTMLElement & { value: string }>): string => (e.target as HTMLElement & { value: string }).value
const MyComponent: FC<VandC<string>> = ({ onChange, value }) => {
const [reload, setReload] = useState(false)
const [state, setstate] = useState(value)
useEffect(() => {
if (reload) {
console.log('called api ')
onChange(state)
setReload(false)
}
}, [reload])
const callApi = () => {
setReload(true)
} // You might be able to call API directly here, I haven't tried
const [debouncedCallApi] = useState(() => _.debounce(callApi, 1000))
function handleChange(x:string) {
setstate(x)
debouncedCallApi()
}
return (<>
<input
value={state} onChange={_.flow(inputValue, handleChange)} />
</>)
}
const delayedHandleChange = debounce(eventData => someApiFunction(eventData), 500);
const handleChange = (e) => {
let eventData = { id: e.id, target: e.target };
delayedHandleChange(eventData);
}