React useState with addEventListener - javascript

Can someone explain to me exactly why the callback from addEventListener after changing value (by click button) in useState from "init" to "new state" shows the value "init" while text in button is "new state"
function Test() {
const [test, setTest] = useState("init")
useEffect(() => {
window.addEventListener("resize", () => console.log(test))
}, [])
return (
<button style={{ marginTop: "100px" }} onClick={() => setTest("new state")}>
{test}
</button>
)
}

Add test to the dependency array in your useEffect:
useEffect(() => {
const func = () => console.log(test);
window.addEventListener("resize", func)
return () => {
window.removeEventListener("resize", func)
}
}, [test])
An explanation of why this is needed is covered in the Hooks FAQ: Why am I seeing stale props or state inside my function?:
another possible reason you’re seeing stale props or state is if you
use the “dependency array” optimization but didn’t correctly specify
all the dependencies. For example, if an effect specifies [] as the
second argument but reads someProp inside, it will keep “seeing” the
initial value of someProp. The solution is to either remove the
dependency array, or to fix it.

Empty dependency array acts as componentDidMount. You should add test as one of the dependencies so it fires again
function Test() {
const [test, setTest] = useState("init")
useEffect(() => {
window.addEventListener("resize", fun)
return () => {
window.removeEventListener("resize", fun)
}
}, [test])
return (
<button style={{ marginTop: "100px" }} onClick={() => setTest("new state")}>
{test}
</button>
)
}
Another reason this is happening is the functional component is using stale state value.
Event is registered once on component mount with useEffect. It's the same function during entire component lifespan and refers to stale state that was fresh at the time when this eventListener was defined the first time.
The solution for that is using refs
Check out https://codesandbox.io/s/serverless-thunder-5vqh9 to understand the solution better

State inside an effect fired once is never updated (if you use [])
The value of "test" inside the scope of your useEffect is never updated since the effect was fired only once at mount.
The State changes you make afterwards will never change this value unless you fire the Effect again but passing an argument into the square brackets.

Related

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)
}
}, [])

Hook useState with useEffect

I encountered this behavior. Two onChange are called in turn. One of them saves data to the state, and use the previus state to generate new one.
const onChange = (data) => {
setState1((prev) => {
console.log("onChange set state");
return [...prev, data];
});
};
The other just prints something to the console.
const onChange2 = (data) => {
console.log("onChange2");
};
onChange and onChange2 called after click on button
<button
onClick={() => {
onChange({ foo: "bar" });
onChange2();
}}
>
2
</button>
useEffect - saves data to state
const [state, setState] = useState(0);
const [state1, setState1] = useState([]);
useEffect(() => {
setState((prev) => {
console.log("use effect set state");
return prev + 1;
});
}, []);
So, if you do not use useEffect (comment out), the console log, which in the first onChange will be called first, then the console log from the second onChange
If you uncomment useEffect, then the console will log from onChange2 first, then from onChange
Why is this happening, who can tell?
CodeSandBox: https://codesandbox.io/s/eager-hooks-sczvb
The call order of onChange and onChange2 appear to change because the console.log in onChange is inside the callback passed to setState1.
If you move the console.log directly in the onChange you will see that the order is the same with or without useEffect.
const onChange = (data) => {
console.log("onChange set state");
setState1((prev) => {
return [...prev, data];
});
};
It's important to understand that when you call setState there is no guarantee that the state will be changed immediately, it's up to React to decide when to apply the new state and trigger a new render.
This is why the callback passed to setState should only return a new state and not run any effects (such as console.log) since the moment this code is run is considered in implementation detail and could change with any version of React.
Expected behavior is onChange2 called first, and onChange set state called second whatever useEffect body is commented. because onChange2 called right now. but onChange set state in setState fn called after rendered。
The "bug" looked is why onChange set state called first when first click. Hmm... find react computeExpirationForFiber. setState callback execute sync or async by different conditions. It's hard to read.

Is there a React hooks feature that has the same functionality as passing a callback as the second argument to setState?

Using hooks, I would like to execute a function only after a particular call to update state. For instance, I would like to achieve the same functionality that this does (assuming I already have already instantiated this piece of state.)
setState({name: Joe}, () => console.log('hi'));
I do not want this to log 'hi' every time that name changes, I only want to log 'hi' after this particular setState call has been executed.
This
const [name, setName] = useState('');
useEffect(() => {
console.log('hi');
}, [name]);
setName('Joe');
setName('Bob'); // only log 'hi' after this one!
setName('Joe');
setName('Bob');
will not work for my purposes because I don't want to log 'hi' every time name changes. The value in the setName call does not matter. The console.log must be executed only after this particular setName call has been executed.
Update: I was overthinking this. I was asking this question because I had a piece of state called "mode" that determined some conditional rendering through a switch statement:
switch(mode) {
case foo: return <Foo />
case bar: return <Bar />
}
I was only wanting to fire some logic when mode was a certain value (aka a certain component would be rendered). I simply moved this logic down a level into the lower-level component and used
React.useEffect(() => {
someLogic();
}, []);
in order to only fire this logic on component render.
State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
Just spitballing here
const useStateWithCallback = val => {
const callback = useRef();
const [state,setState] = useState(val);
useEffect(() => {
callback.current && callback.current(state);
callback.current = null;
},[callback,state]);
return [
state,
useCallback((arg,cb) => {
callback.current = cb;
return setState(arg);
},[callback,setState])) // these are safe to omit, right?
]
}
EDIT: not to be too verbose, but usage:
import { useStateWithCallback } from './useStateWithCallback';
const MyCmp = () => {
const [name,setName] = useStateWithCallback('');
...
setName('joe',state => console.log(`Hi ${state}`));
...
}

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.

how update the value of useEffect() hook on button click

I have a functional component and I have created a button inside it. I am also using a "Use_effect()" hook. My main is to re-render the functional component, update the use_effect() hook when the button is clicked.
const Emp_list = (props) => {
useEffect(() => {
props.getList(props.state.emp);
}, []);
return (
<div>
{props.state.emp.map((val ) =>
{val.feature_code}
{val.group_code}
<button onClick = {() => props.removeEmpFromList(val.feature_code)} > Remove </button>
<EmpForm empList={props.state.emp}
onChangeText = {props.onChangeText}
/>
</div>
<button onClick= {() => props.getdata (props.state)}>Get Names</button>
<p>
</div>
);
};
export default Emp_list;
removeEmpFromList = (i) => {
const remaining = this.state.emp( c => c.feature_code !== i)
this.setState({
emp: [...remaining]
})
}
When I click the Remove button , it will basically remove the employee from the list. The function removeEmpFromList will update the state.
The functional component EmpForm basically shows the list of all employees.
So I want to re-render the page so that, it updates the state value in useEffect() hook. So when EmpForm is called again on re-rending it shows the updated list.
You didn't provide the code for removeEmpFromList() ... but probably it updates the state by mutation therefor component gets the same object ref - compared shallowly - no difference, no reason to rerender.
Modify removeEmpFromList() method to create a new object for emp - f.e. using .filter.
If not above then passing entire state is the source of problem (the same reason as above).
Simply pass only emp as prop or use functions in setState() (to return a new object for the entire state) this way.
I figured it out! Thanks for the help guys.
So, it was not re-rendering because initally, useEffect() second parameter was [] , if you change it to props.state then it will update the changes made to the state and re-render the component automatically.
useEffect(() => {
props.getList(props.state.emp);
}, [props.state.emp]);

Categories

Resources