State Value Stops Updating Within UseEffect Hook - Gatsby/React - javascript

State value showMenu is not updating within useEffect hook.
When testing, when the button is first clicked and the screen is touched to move, showMenu properly consoles to true. When the button is clicked a second time (and third, forth, etc) and the screen is touched to move, showMenu continues to console as true when it should alternate to false.
const [showMenu, setShowMenu] = useState(false)
useEffect(_ => {
const listener = e => {
e.preventDefault()
console.log(showMenu, ' useEffect - touchmove')
}
showMenu
? document.body.addEventListener('touchmove', listener, {passive: false})
: document.body.removeEventListener('touchmove', listener)
}, [showMenu])
return (
<button onclick={_ => {
console.log(!showMenu, ' button click')
setShowMenu(!showMenu)
}} />
)
Console Result

I think the event of body is not properly removed, because listener is changed every time useEffect.
So you can return a function in useEffect to clear the previous useEffect.
useEffect(() => {
if (showMenu) {
const listener = e => {
e.preventDefault();
console.log(showMenu, ' useEffect - touchmove');
};
document.body.addEventListener('touchmove', listener, { passive: false });
return () => {
document.body.removeEventListener('touchmove', listener);
}
}
}, [showMenu]);
You can also read cleaning-up-an-effect to learn more

I don't know what your intent is, but what you are doing with useEffect is probably not what you're expecting. When showMenu is false, you're removing a listener function that has not been bound because objects are compared by reference in JS and listener is being redefined each time showMenu changes.
The typical way to unbind a listener when useEffect changes is to return a function that handles the cleanup from your useEffect callback. Like so:
useEffect(() => {
const listener = e => {
e.preventDefault()
console.log(showMenu, ' useEffect - touchmove')
}
document.body.addEventListener('touchmove', listener, { passive: false })
return () = {
document.body.removeEventListener('touchmove', listener, { passive: false })
}
}, [showMenu])

Related

State is <empty string> when function is called on key event

In React, I have a number of buttons (imagine a PIN layout with numbers) that update the state on click. I also added an event listener to the document so pressing keys on the keyboard updates the pin too. However, there's a strange problem. When I add a number by clicking a button, the state is working correctly and everything is fine, but when I press a key on a physical keyboard, the state updates, but logs as <empty string>!
Here is the code:
export default function Keypad() {
const [pin, setPin] = useState("");
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
}
I guess this is because document can't access the pin state, but if it was the case, the setPin shouldn't work either. Am I right?
Your component does not keep a reference when listening to DOM events, this answer has some neat code for listening to window events using a fairly simple hook. When applied to your code, it works as expected:
const {useState, useEffect, useRef} = React;
// Hook
function useEventListener(eventName, handler, element = window){
// Create a ref that stores handler
const savedHandler = useRef();
// Update ref.current value if handler changes.
// This allows our effect below to always get latest handler ...
// ... without us needing to pass it in effect deps array ...
// ... and potentially cause effect to re-run every render.
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// Make sure element supports addEventListener
// On
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// Create event listener that calls handler function stored in ref
const eventListener = event => savedHandler.current(event);
// Add event listener
element.addEventListener(eventName, eventListener);
// Remove event listener on cleanup
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // Re-run if eventName or element changes
);
};
const Keypad = (props) => {
const [pin, setPin] = useState([]);
function addNumber(num) {
console.log(pin); // returns the correct pin with handleKeyClick, returns <empty string> with handleKeyDown
if (pin.length < 6) { // only works if the pin is not <empty string>
setPin((pin) => [...pin, num.toString()]); // works correctly with both handleKeyClick and handleKeyDown even if pin logged <empty string>!
}
}
function handleKeyClick(num) {
addNumber(num);
}
function handleKeyDown(e) {
if (!isNaN(e.key)) {
addNumber(e.key);
}
}
useEventListener("keydown", handleKeyDown)
return (
<div>
{/* just one button for example */}
<button onClick={() => handleKeyClick(9)}>9</button>
</div>
)
return "Hello World"
}
ReactDOM.render(<Keypad />, document.getElementById("root"))
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

missing dependency warning when working with event listeners and states inside useEffect

Everytime I work with addEventListener(), and also want to access some state inside useEffect, I get the same issue. I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
I almost everytime find myself stuck with the "React Hook useEffect has a missing dependency" warning.
Let's say I have a component that needs to change it state on window.onClick() and on window.onDoubleClick(). If the state is true, click should change it to false, and if the state is false, double click should change it to true.
So here's what I whould write:
import React, { useState, useEffect } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
useEffect(() => {
window.addEventListener('click', (event) => {
if (toggle) setToggle(false)
})
window.addEventListener('dblclick', (event) => {
if (!toggle) setToggle(true)
})
}, [])
return (
<p>The toggle state is {toggle.toString()}</p>
);
}
This code works, but I get the missing dependency warning. I can't add toggle to the dependency array, because then it will add another event listener each time the toggle state changes.
What am I doing wrong here? how should I fix this?
Edit: Maybe this example wasn't too good, but it's the simplest I could think of. But, this issue is also for when I create other event listeners, that have to be on the windows object, like scroll. I know I can use return to remove the event listener everytime, but for events like scroll it makes it much slower. It doesn't make sense to me that I have to remove and add it everytime, when I just don't need it to fire again.
With react you don't have to use the window element in this case. Not even a useEffect.
By using the useEffect hook you are telling react to do something after render (depending on the dependency array). In this case changing state is not necessary immediately after rendering the page, only when the user interacts with the element.
Adding click events through the useEffect is probably not needed most of the time and and doing it like the example below will probably save you time and a headache and maybe even performance (correct me if i'm wrong).
I would personally do it like this.
import React, { useState } from 'react';
export default function someComponent() {
const [toggle, setToggle] = useState(false);
return (
<p
onClick={() => setToggle(false)}
onDoubleClick={() => setToggle(true)}
>
The toggle state is {toggle.toString()}
</p>
);
}
You could also call functions from the element like so
const [toggle, setToggle] = useState(false);
const handleClick = () => {
if (toggle) {
setToggle(false);
}
};
const handleDoubleClick = () => {
if (!toggle) {
setToggle(true);
}
};
return (
<p
onClick={() => handleClick()}
onDoubleClick={() => handleDoubleClick()}
>
The toggle state is {toggle.toString()}
</p>
);
CodeSandbox example
You can add a clean-up function to the useEffect hook to remove old listeners. This way you can pass toggle into the dependency array and you won't have stacking event listeners.
https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
const handleClick = () => toggle ? setToggle(false) : setToggle(true);
window.addEventListener('click', handleClick);
window.addEventListener('dblclick', handleClick);
return () => {
window.removeEventListener('click', handleClick);
window.removeEventListener('dblclick', handleClick);
}
}, [toggle]);
I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.
There is a way around this, and that is to return a cleanup function from the useEffect callback. I would encourage you to read the linked section of the docs, then the below solution would become much clearer:
useEffect(() => {
const handleClick = () => {
setToggle(!toggle)
}
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [toggle])
with the above solution, each time toggle is updated, the cleanup function is called, which removes the current event listener before running the effect again.
Also note that you can provide a callback function to setToggle, which receives the current value of toggle and returns the new value. With this approach you wouldn't need to pass toggle as a dependency to useEffect:
useEffect(() => {
const handleClick = () => {
setToggle(currentValue => !currentValue)
}
window.addEventListener("click", handleClick)
return () => {
window.removeEventListener("click", handleClick)
}
}, [])

How to make useEffect listening to any change in localStorage?

I am trying to have my React app getting the todos array of objects from the localStorage and give it to setTodos. To do that I need to have a useEffect that listen to any change that occurs in the local storage so this is what I did:
useEffect(() => {
if(localStorage.getItem('todos')) {
const todos = JSON.parse(localStorage.getItem('todos'))
setTodos(todos);
}
}, [ window.addEventListener('storage', () => {})]);
The problem is that useEffect is not triggered each time I add or remove something from the localStorage.
Is this the wrong way to have useEffect listening to the localStorage?
I tried the solution explained here but it doesn't work for me and I sincerely I do not understand why it should work because the listener is not passed as a second parameter inside the useEffect
You can't re-run the useEffect callback that way, but you can set up an event handler and have it re-load the todos, see comments:
useEffect(() => {
// Load the todos on mount
const todosString = localStorage.getItem("todos");
if (todosString) {
const todos = JSON.parse(todosString);
setTodos(todos);
}
// Respond to the `storage` event
function storageEventHandler(event) {
if (event.key === "todos") {
const todos = JSON.parse(event.newValue);
setTodos(todos);
}
}
// Hook up the event handler
window.addEventListener("storage", storageEventHandler);
return () => {
// Remove the handler when the component unmounts
window.removeEventListener("storage", storageEventHandler);
};
}, []);
Beware that the storage event only occurs when the storage is changed by code in a different window to the current one. If you change the todos in the same window, you have to trigger this manually.
const [todos, setTodos] = useState();
useEffect(() => {
setCollapsed(JSON.parse(localStorage.getItem('todos')));
}, [localStorage.getItem('todos')]);

React state resets when set from prop event handler

Cannot for the life of me figure out what is going on, but for some reason when "Click me" is clicked, the number increments as you'd expect. When a click is triggered by the Child component, it resets the state and ALWAYS prints 0.
function Child(props: {
onClick?: (id: string) => void,
}) {
const ref = useCallback((ref) => {
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [])
return (<div ref={ref}></div>)
}
function Parent() {
const [number, setNumber] = useState(0);
return <div>
<div onClick={() => {
setNumber(number + 1);
console.log(number);
}}>
This Works
</div>
<Child
onClick={(id) => {
setNumber(number + 1);
console.log(number);;
}}
/>
</div>
}
And here is a demonstration of the problem: https://jscomplete.com/playground/s333177
Both the onClick handlers in parent component are re-created on every render, rightly so as they have a closure on number state field.
The problem is, the onClick property sent to Child component is used in ref callback, which is reacted only during initial render due to empty dependency list. So onClick prop received by Child in subsequent renders does not get used at all.
Attempt to resolve this error, by either removing dependency param or sending props.onClick as in dependency list, we land into issue due to caveat mentioned in documentation. https://reactjs.org/docs/refs-and-the-dom.html
So you add null handling, and you see your updated callback now getting invoked, but... all earlier callbacks are also invoked as we have not removed those event listeners.
const ref = useCallback((ref) => {
if(!ref) return;
ref.innerHTML = 'This Doesnt';
ref.addEventListener('click',() => {
props.onClick!('')
})
}, [props.onClick])
I believe this just an experiment being done as part of learning hooks, otherwise there is no need to go in roundabout way to invoke the onClick from ref callback. Just pass it on as prop to div.
Edit:
As per your comment, as this is not just an experiment but simplification of some genuine requirement where click handler needs to be set through addEventListener, here is a possible solution:
const ref = useRef(null);
useEffect(() => {
if(!ref.current) return;
ref.current.innerHTML = 'This Doesnt';
const onClick = () => props.onClick!('');
ref.current.addEventListener('click',onClick)
// return the cleanup function to remove the click handler, which will be called before this effect is run next time.
return () => {ref.current.removeEventListener("click", onClick)}
}, [ref.current, props.onClick]);
Basically, we need to use useEffect so that we get a chance to remove old listener before adding new one.
Hope this helps.
function Child(props: {
onClick?: (id: string) => void,
}) {
function handleClick() {
props.onClick('')
}
const ref = useRef();
useEffect(() => {
ref.current.innerHTML = 'This Doesnt';
ref.current.addEventListener('click', handleClick)
return () => { ref.current.removeEventListener('click', handleClick); }
}, [props.onClick])
return (<div ref={ref}></div>)
}
#ckder almost works but console.log displayed all numbers from 0 to current number value.
Issue was with event listener which has not been remove after Child component umount so to achive this I used useEffect and return function where I unmount listener.

React hooks - setState does not update state properties

I have a event binding from the window object on scroll. It gets properly fired everytime I scroll. Until now everything works fine. But the setNavState function (this is my setState-function) does not update my state properties.
export default function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e: any) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('currScrollPos: ', currScrollPos); // updates accordingly to the scroll pos
console.log('lastScrollPos: ', lastScrollPos); // last scroll keeps beeing 0
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
}, []);
...
}
So my question is how do I update my state properties with react hooks in this example accordingly?
it's because how closure works. See, on initial render you're declaring handleScroll that has access to initial navState and setNavState through closure. Then you're subscribing for scroll with this #1 version of handleScroll.
Next render your code creates version #2 of handleScroll that points onto up to date navState through closure. But you never use that version for handling scroll.
See, actually it's not your handler "did not update state" but rather it updated it with outdated value.
Option 1
Re-subscribing on each render
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
});
Option 2
Utilizing useCallback to re-create handler only when data is changed and re-subscribe only if callback has been recreated
const handleScroll = useCallback(() => { ... }, [navState]);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
Looks slightly more efficient but more messy/less readable. So I'd prefer first option.
You may wonder why I include navState into dependencies but not setNavState. The reason is - setter(callback returned from useState) is guaranteed to be referentially same on each render.
[UPD] forgot about functional version of setter. It will definitely work fine while we don't want to refer data from another useState. So don't miss up-voting answer by giorgim
Just add dependency and cleanup for useEffect
function TabBar() {
const [navState, setNavState] = React.useState(
{
showNavLogo: true,
lastScrollPos: 0
});
function handleScroll(e) {
const currScrollPos = e.path[1].scrollY;
const { lastScrollPos, showNavLogo } = navState;
console.log('showNavLogo: ', showNavLogo);
console.log('lastScrollPos: ', lastScrollPos);
if (currScrollPos > lastScrollPos) {
setNavState({showNavLogo: false, lastScrollPos: currScrollPos});
} else {
setNavState({showNavLogo: true, lastScrollPos: currScrollPos});
}
}
React.useEffect(() => {
window.addEventListener('scroll', handleScroll.bind(this));
return () => {
window.removeEventListener('scroll', handleScroll.bind(this));
}
}, [navState]);
return (<h1>scroll example</h1>)
}
ReactDOM.render(<TabBar />, document.body)
h1 {
height: 1000px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
I think your problem could be that you registered that listener once (e.g. like componendDidMount), now every time that listener function gets called due to scroll, you are referring to the same value of navState because of the closure.
Putting this in your listener function instead, should give you access to current state:
setNavState(ps => {
if (currScrollPos > ps.lastScrollPos) {
return { showNavLogo: false, lastScrollPos: currScrollPos };
} else {
return { showNavLogo: true, lastScrollPos: currScrollPos };
}
});

Categories

Resources