Handle scroll event will fire to often. What is the way to slowdown/debounce it?
And if it's possible, i want last event always be fired and not skipped.
const handleScroll = event => {
//how to debounse scroll change?
//if you will just setValue here, it's will lag as hell on scroll
}
useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
Here is the useDebounce hook example from xnimorz
import { useState, useEffect } from 'react'
export const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
},
[value, delay]
)
return debouncedValue
}
Event handler that uses hooks can be debounced done the same way as any other function with any debounce implementation, e.g. Lodash:
const updateValue = _.debounce(val => {
setState(val);
}, 100);
const handleScroll = event => {
// process event object if needed
updateValue(...);
}
Notice that due to how React synthetic events work, event object needs to be processed synchronously if it's used in event handler.
last event always be fired and not skipped
It's expected that only the last call is taken into account with debounced function, unless the implementation allows to change this, e.g. leading and trailing options in Lodash debounce.
const debounceLoadData = useCallback(debounce((debounceValue)=>{
props.setSenderAddress(debounceValue)
}, 300), []);
Related
I'm checking if a component is unmounted, in order to avoid calling state update functions.
This is the first option, and it works
const ref = useRef(false)
useEffect(() => {
ref.current = true
return () => {
ref.current = false
}
}, [])
....
if (ref.current) {
setAnswers(answers)
setIsLoading(false)
}
....
Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
return () => {
setIsMounted(false)
}
}, [])
....
if (isMounted) {
setAnswers(answers)
setIsLoading(false)
}
....
Why is the second option not working compared with the first option?
I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.
import { useCallback, useEffect, useRef } from "react";
export function useIsMounted() {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
Usage
function MyComponent() {
const [data, setData] = React.useState()
const isMounted = useIsMounted()
React.useEffect(() => {
fetch().then((data) => {
// at this point the component may already have been removed from the tree
// so we need to check first before updating the component state
if (isMounted()) {
setData(data)
}
})
}, [...])
return (...)
}
Live Demo
Please read this answer very carefully until the end.
It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:
}, [isMounted])
Now, it watches the state and run the effect on every update. But why the first option works?
It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:
This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.
BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:
useEffect(() => {
setIsMounted(true)
}, []) // you may watch isMounted state
// if you're changing it's value from somewhere else
While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.
This is a typescript version of #Nearhuscarl's answer.
import { useCallback, useEffect, useRef } from "react";
/**
* This hook provides a function that returns whether the component is still mounted.
* This is useful as a check before calling set state operations which will generates
* a warning when it is called when the component is unmounted.
* #returns a function
*/
export function useMounted(): () => boolean {
const mountedRef = useRef(false);
useEffect(function useMountedEffect() {
mountedRef.current = true;
return function useMountedEffectCleanup() {
mountedRef.current = false;
};
}, []);
return useCallback(function isMounted() {
return mountedRef.current;
}, [mountedRef]);
}
This is the jest test
import { render, waitFor } from '#testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";
describe("useMounted", () => {
it("should work and not rerender", async () => {
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
callback(isMounted())
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
expect(callback.mock.calls).toEqual([[true]])
unmount();
expect(callback.mock.calls).toEqual([[true]])
})
it("should work and not rerender and unmount later", async () => {
jest.useFakeTimers('modern');
const callback = jest.fn();
function MyComponent() {
const isMounted = useMounted();
useEffect(() => {
(async () => {
await delay(10000);
callback(isMounted());
})();
}, [])
return (<div data-testid="test">Hello world</div>);
}
const { unmount } = render(<MyComponent />)
await waitFor(() => expect(callback).toBeCalledTimes(0));
jest.advanceTimersByTime(5000);
unmount();
jest.advanceTimersByTime(5000);
await waitFor(() => expect(callback).toBeCalledTimes(1));
expect(callback.mock.calls).toEqual([[false]])
})
})
Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted
This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.
import React from 'react'
const MyComponent = () => {
const [fooState, setFooState] = React.useState(null)
React.useEffect(()=> {
//Mounted
getFetch()
// Unmounted
return () => {
setFooState(false)
}
})
return (
<div>Stuff</div>
)
}
export {MyComponent as default}
If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:
import React from 'react'
import {useIsMounted} from 'react-tidy'
function MyComponent() {
const [data, setData] = React.useState(null)
const isMounted = useIsMounted()
React.useEffect(() => {
fetchData().then((result) => {
if (isMounted) {
setData(result)
}
})
}, [])
// ...
}
Learn more about this hook
Disclaimer I am the writer of this library.
Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.
import {useRef, useCallback, useEffect} from "react";
export function useIsMounted(): () => boolean {
const isMountedRef = useRef(true);
const isMounted = useCallback(() => isMountedRef.current, []);
useEffect(() => {
isMountedRef.current = true;
return () => void (isMountedRef.current = false);
}, []);
return isMounted;
}
It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.
I'm working on creating a custom hook that captures the browser window size to let me know if it's mobile or not. At the moment, my issue is React telling me it can't retain the variable value of screenSize within the useEffect hook. How do I get around this?
export default function useIsMobile() {
let screenSize = 0;
useEffect(() => {
window.addEventListener("resize", () => {
screenSize = window.innerWidth;
});
return () => {
window.removeEventListener("resize", () => {
screenSize = window.innerWidth;
})
}
}, [screenSize]);
return screenSize <= 768;
}
You can use the useRef hook to create a variable on the component level, then use the .current property to update it's value.
export default function useIsMobile() {
const screenSize = useRef();
useEffect(() => {
window.addEventListener("resize", () => {
screenSize.current = window.innerWidth;
});
return () => {
window.removeEventListener("resize", () => {
screenSize.current = window.innerWidth;
})
}
}, []);
return screenSize.current <= 768;
}
Note that I also removed the dependency from the useEffect hook because it's not needed. Because it's a ref and not a state, you will use the same variable every time which means that you don't need to re-register the listener.
UPDATE
The way useRef work will not trigger a re-render which is why there is a need to use a useState hook instead.
This code snippet will cause a re-render every time the mode changes.
const getIsMobile = () => window.innerWidth <= 768;
export default function useIsMobile() {
const [isMobile, setIsMobile] = useState(getIsMobile());
useEffect(() => {
const onResize = () => {
setIsMobile(getIsMobile());
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
}
}, []);
return isMobile;
}
Note that I moved getIsMobile outside the component because it doesn't contain any logic related to it and also so it could be called on the default value of the useState hook to save a re-render right when the hook first loads.
I cannot find how to solve one issue could anyone help
wrote simple hook for catching shortcuts but callback from that custom hook doesn't see new props in component
export const useShortcut = (key, isAlt = false, callback) => {
function onKeyPressed(event) {
if (event.key.toLowerCase() === key && event.altKey === isAlt) {
callback();
}
}
useEffect(() => {
window.addEventListener("keydown", onKeyPressed);
return () => {
window.removeEventListener("keydown", onKeyPressed);
};
}, []);
};
export const MyComponent = (props) => {
function handleShortcut() {
if (props.prop1) {
//???prop1 came from parent and components tab tell me but from shortcut hook it always null
}
}
useShortcuts("n", true, () => handleShortcut());
return <div></div>;
};
many thanks in advance
useEffect(() => {
window.addEventListener("keydown", onKeyPressed);
return () => {
window.removeEventListener("keydown", onKeyPressed);
};
}, []);
Because of the empty dependency array, you are only setting up the listener once, using whatever onKeyPressed exists on the first render. That function closes over props from the first render, and it will never update.
Simplest fix for this is just to remove the dependency array, so the effect runs every time:
useEffect(() => {
window.addEventListener("keydown", onKeyPressed);
return () => {
window.removeEventListener("keydown", onKeyPressed);
};
});
Now when the component renders, it will tear down the old listener and create a new one with the new copy of onKeyPressed. That new function sees the new props.
Hypothetically, if setting up and tearing down the listeners was an expensive operation (it's not), then you could limit it to only happen when onKeyPressed changes, by putting onKeyPressed into the dependency array. However, this needs to be accompanied by using useCallback to make sure onKeyPressed doesn't change unless it needs to.
useEffect(() => {
window.addEventListener("keydown", onKeyPressed);
return () => {
window.removeEventListener("keydown", onKeyPressed);
};
}, [onKeyPressed]);
// used like:
export const MyComponent = (props) => {
const handleShortcut = useCallback(function () {
if (props.prop1) {
// ...
}
}, [props.prop1]);
useShortcuts("n", true, handleShortcut);
I've got stuck with a problem of hook reusage when state and redux store should work together. I've simplified a code to show the problem.
There is a component where I want to use multiple hooks (for simplicity I re-use useMouseDown hook here):
export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const plusSighRef = useRef<HTMLButtonElement>(null);
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
console.log('in first hook');
dispatch(increment());
}
});
useMouseDown({
ref: plusSighRef,
onMouseDown: () => { console.log('in second hook'); }
});
return <button ref={plusSighRef}>+</button>;
}
Each hook has inner state and has own callback on mouse down event:
const useMouseDown = ({ ref, onMouseDown }) => {
const [isClicked, setIsClicked] = useState(false);
useEffect(() => {
const element = ref.current;
const down = (e: MouseEvent) => {
setIsClicked(true);
onMouseDown(e);
}
element.addEventListener('mousedown', down);
return (): void => {
element.removeEventListener('mousedown', down);
};
}, [onMouseDown, ref]);
}
As a result a mousedown event in second hook is never triggered. The problem is that re-render occurs earlier than second hook is started.
I found some solutions but don't like both:
use something like setTimeout(() => dispatch(increment()), 0) inside the first hook mousedown prop. But it seems to be not obvious in terms of re-usage.
rewrite two hooks into one and manipulate with one "big" mousedown handler. But in that case a combined hook could be difficult for maintaining.
So I need a solution that allow to retain structure as is (I mean two separate hooks), but has second hook is working too. Could someone help how to get it?
Can't you just keep your logic in two different functions and execute it sequentially in the hook's onMouseDown function?
const executeFirstFlow = () => {
console.log('in first function');
dispatch(increment());
};
const executeSecondFlow = () => {
console.log('in second function');
};
useMouseDown({
ref: plusSighRef,
onMouseDown: () => {
executeFirstFlow();
executeSecondFlow();
}
});
I have a stateless component which needs to listen on keyboard event. It adds keydown listener when the component is mounted and remove it when the component is unmounted. There is a state test is boolean value. It is set to true when the component is mounted. But in the keydown event listener, its value always false. It looks like the listener doesn't take the state reference. What's wrong with my code?
const { useEffect, useState } = React;
const Comp = () => {
const [test, setTest] = useState(false);
const keyPressHandler = (e) => {
setTest(!test);
console.log(test);
}
useEffect(() => {
setTest(true);
window.addEventListener('keydown', keyPressHandler);
return () => {
window.removeEventListener('keydown', keyPressHandler);
};
}, []);
return (
<div className="test">
hello {test + ""}
</div>
);
};
You can re-run useEffect when test change which will make the listener always have the current value.
useEffect(() => {
setTest(true);
window.addEventListener('keydown', keyPressHandler);
return () => {
window.removeEventListener('keydown', keyPressHandler);
};
}, [test]);
and you should remove the setTest from the useEffect since it will keep changing test to true whenever you use setTest().