I'm trying to wrap my head around this. My custom hook is supposed to create a simple popup with the desired input and remove after 3 seconds. Of course, currently, it re-renders every time the counter has reset. How can I make it render only once and then be removed from the dom?
export function createPopup(content, popupType) {
const [message, setMessage] = useState(content)
const [type, setType] = useState(popupType)
const [value, setCounter] = useState(3)
const myTimer = setTimeout(() => {
return setCounter(value - 1)
}, 1000)
useLayoutEffect(() => {
const id = setTimeout(() => {
setCounter(value + -1);
}, 1000);
return () => {
clearTimeout(id);
};
}, [value])
return (
<>
{value !== 0 &&
<PopupMolecule type={type}>
{message}
</PopupMolecule>
}
</>
)
}
I think you want something more like this:
export function createPopup(content, popupType) {
const [visible, setVisible] = useState(true);
useEffect(() => {
setTimeout(() => setVisible(false), 3000);
}, []);
return (
<>
{visible &&
<PopupMolecule type={popupType}>
{content}
</PopupMolecule>
}
</>
)
}
There are still some improvements that need to made here (i.e. fading out on exit or some transition, and the way this is setup you can't use it more than once), but this should fix the problem you stated.
This will show your popup for three seconds on mount, then make your popup invisible and unmount it from the DOM. The empty array in the useEffect hook lets it know to only trigger on mount (and unmount if you return a value). You also don't need the other state variables that you're not updating. Those can just be passed in as function parameters.
Related
This should be fairly simple, but I keep getting a weird behaviour from the result.
Basically, I have an array of images:
const images = [img1, img2, img3, img4, img5, img6];
I also have an image index:
const [imageIndex, setImageIndex] = useState(0);
Then I do a little incrementation of the index:
const switchImage = () => {
if (imageIndex === images.length - 1) {
setImageIndex(0);
} else {
setImageIndex(imageIndex + 1);
}
return imageIndex;
}
Then I call this function from a useEffect:
useEffect(() => {
setInterval(() => {
switchImage();
}, 1000);
}, []);
And finally I add the html:
<img src={images[imageIndex]} />
The result is usually it gets stuck on the second image and stops incrementing, so I thought the issue might be with the useEffect and the way the component is rendering.
You need to use the second method signature of the useState setter function which gives you the previous state value to avoid the stale closure captured value.
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['1','2','3','4','5','6'];
const Thing =()=>{
const [imageIndex, setImageIndex] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setImageIndex(prev => (
prev === images.length - 1 ? 0 : prev + 1
));
}, 1000);
},[])
console.log(imageIndex)
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render(<Thing />);
See here https://codepen.io/drGreen/pen/JjpmQrV
Also worth seeing this link which is virtually identical.
In your case the useEffect which you have created it is only being triggered once; when the component is loading - that is because you did not define when this logic should be triggered by adding dependencies to the useEffect.
Now, since the component renders once, 'switchImage'()' is only being triggered once, hence, it iterates once, display the img and stops.
Here is some good documentation on useEffect if you would like to read more about it Using the Effect Hook - React
๐กHere is a slightly altered solution where we are using the debounce technique for the timer. SOLUTION๐ก
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['๐ก','๐','๐','๐','๐ฏ','๐'];
const DemoComponent = () =>{
const [imageIndex, setImageIndex] = React.useState(0);
//debounce set default 0.3s
const debounce = (func, timeout = 300) =>{
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
// switch img fn.
const switchImage = () => {
setImageIndex(imageIndex === images.length - 1 ? 0 : imageIndex + 1)
return imageIndex;
}
//debounce switchImage and set timer to 1s
const switchImageDebounce = debounce(() => switchImage(),1000);
//useEffect
React.useEffect(() => {
switchImageDebounce()
}, [imageIndex]);
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render();
I've seen quite a few similar questions here, but none of the suggested solutions seemed to be working for me. Here's my problem (please notice that I'm very new to React, just learning it):
I have the following code in my App.js:
function App() {
const [movieSearchString, setMovieSearchString] = useState('')
const providerValue = {
moviesList: moviesList,
movieSearchString: movieSearchString,
}
const searchMovieHandler = () => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}
const movieInputChangeHandler = string => {
console.log('onMovieInputChange', string);
setMovieSearchString(string)
searchMovieHandler()
}
return (
<MoviesContext.Provider value={providerValue}>
<div className="App d-flex flex-column px-4 py-2">
<SearchBar
onMovieInputChange={movieInputChangeHandler}
/>
...Rest of the content
</div>
</MoviesContext.Provider>
);
}
In this situation all the console.logs get called EXCEPT the one that should be debounced (I tried both lodash debounce and my own, none worked; currently kept the lodash version).
So I tried to comment out that debounce call and tried to use it like that:
useEffect(() => {
console.log('use effect 1')
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}, [movieSearchString])
I'm getting the use effect 1 log when the movieSearchString changes, but not the debounced one.
So I tried to do this:
const debounced = useRef(debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000))
useEffect(() => debounced.current(movieSearchString), [movieSearchString])
In this case I'm getting the console log deb: after a second, but no movieSearchString is printed.
I don't know what else I can do here... Eventually what I want is when a user enters something in the text field, I want to send an API call with the entered string. I don't want to do it on every key stroke, of course, thus need the debounce. Any help, please?
Try to debounce the state value, not to debounce effect itself. It's easier to understand.
For example, you can use custom hook useDebounce in your project:
useDebounce.js
// borrowed from https://usehooks.com/useDebounce/
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
App.js
const [movieSearchString, setMovieSearchString] = useState('')
const debouncedMovieSearchString = useDebounce(movieSearchString, 300);
const movieInputChangeHandler = string => {
setMovieSearchString(string)
}
useEffect(() => {
console.log('see useDebounceValue in action', debouncedMovieSearchString);
}, [debouncedMovieSearchString]);
useEffect(() => {
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = debouncedMovieSearchString
}
callApi(params);
}, [debouncedMovieSearchString]);
Refer to this article: https://usehooks.com/useDebounce/
You need to wrap the function you want to debounce wit the debounce() function like this:
const searchMovieHandler = debounce(() => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
}, 1000);
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.
I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}
Do all variables used inside a useEffect always, absolutely, without exception need to be specified as dependencies?
My use case (simplified for demonstration purposes) involves different functions being executed depending on the width of the browser window, but not run when browser window changes:
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
useEffect(() => {
isDesktop ? scrollToTop() : scrollToTopOfArticle()
}, [selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
);
}
If I add isDesktop to the dependencies then the effect runs whenever the user resizes the window between mobile and desktop, which is not desired, but I'm also aware of the dogma that everything used inside the effect must be listed as a dependency.
Any suggestions on how to reconcile these two requirements?
If you want to make a useEffect() only responsive to a change in selectedArticle, use isDesktop and selectedArticle to initialize component states. Whenever selectedArticle changes, the first useEffect() will update both states with the passed-in props, and trigger the second useEffect() to re-run on the next render.
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const [desktop, setDesktop] = useState(isDesktop)
const [article, setArticle] = useState(selectedArticle)
useEffect(() => {
if (article !== selectedArticle) {
setDesktop(isDesktop)
setArticle(selectedArticle)
}
}, [isDesktop, selectedArticle, article])
useEffect(() => {
if (desktop) scrollToTop()
else scrollToTopOfArticle()
}, [desktop, article])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Alternatively, you can abstract this latching behavior to another hook so that isDesktop only updates to its live value when selectedArticle changes. Note that selectedArticle still needs to be a dependency of the scroll action effect, so that the useEffect() will trigger the scroll action on every change to selectedArticle even if isDesktop has not changed values since the last trigger.
const useLatch = (value, deps) => {
const [state, setState] = useState(value)
const effect = useCallback(() => { setState(value) }, [value])
useEffect(effect, deps)
return state
}
const scrollToTop = () => window.scrollTo(0, 0)
const scrollToTopOfArticle = () => window.scrollTo(0, 200)
function App({
isDesktop,
selectedArticle
}) {
const latchedIsDesktop = useLatch(isDesktop, [selectedArticle])
useEffect(() => {
if (latchedIsDesktop) scrollToTop()
else scrollToTopOfArticle()
}, [latchedIsDesktop, selectedArticle])
return (
<div className="App">
<h1>{selectedArticle.title}</h1>
<p>{selectedArticle.body}</p>
</div>
)
}
Are all variables inside a useEffect absolutely required to be listed as dependencies?
Yes or else it will generate a warning.
That's why it's best to have clear idea what each effect should do (i.e separation of concern).
Thus, you can have two separate effects with different dependencies.
Something like:
useEffect(
() => {
scrollToTopOfArticle();
}, [selectedArticle]
);
useEffect(
() => {
if (selectedArticle && isDesktop) {
scrollToTop();
}
}, [isDesktop, selectedArticle]
)