Why useState hook changing the state as the previous click? [duplicate] - javascript

This question already has answers here:
Why does calling react setState method not mutate the state immediately?
(9 answers)
Closed 1 year ago.
I was using useState hook and it seems that the hook is not working properly. When I click on one of my radio buttons initially it logs nothing, but after my second click it logs the previous one that I clicked. Every time I click on any button It logs the button that I previously clicked. #sidenote: (The 'info' that I've imported is an array of objects and each 'incorrect_answers' property has an array as value) . Here is my code:
import React, {useState} from 'react'
import { info } from "./data";
const Quiz = () => {
const [i, setI] = useState(0);
const [value, setValue] = useState('');
const {correct_answer, incorrect_answers} = info[i]
const arr = [correct_answer, ...incorrect_answers].sort((a, b) => a.length - b.length);
console.log(arr)
const handleSubmit = (e) => {
e.preventDefault();
setI(i + 1);
}
const handleChange = (e) => {
setValue(e.target.value);
console.log(value);
}
return (
<div className="quiz">
<form className='quiz__form' onSubmit={(e) => handleSubmit(e)}>
<div className="form__body" >
{arr.map((item, index) => {
return (
<div className="form__item" key={index}>
<label htmlFor={`option-${index}`}>
<input
type="radio"
name="options"
id={`option-${index}`}
value={item}
onClick={(e) => handleChange(e)}
/> {item}
</label>
</div>
)
})}
</div>
</form>
</div>
)
}
Can anyone tell me where I am wrong?

setValue is asynchronus. So you will have to wait to see the change. One thing you can do to see the change is add this following code
useEffect(() => {
console.log(value)
}, [value])
This way you can see when the value of the value changes

The setState function in reactjs (setValue in your case) is an asynchronous function, it update the state at the end of the event handler (handleChange), so the value variable wil still refer the previous state inside the event handler (handleChange).
Try to add console.log before the returned JSX, then click on input radio you see log after the event handler was executed
const handleChange = (e) => {
setValue(e.target.value);
console.log(value);
}
console.log("value after handle event",value);
return (
For more information about hou reactjs update state please check here

Related

ReactJS list of components

I am new in JS and ReactJS.
I tried to implement a list of boxes as follows:
import React, { useState, useEffect, useRef } from "react";
import "./App.css";
function GenreBox() {
let [input, setInput] = useState('')
let [repr, setRepr] = useState('')
let getClick = () => {
fetch(`/genre/${input}`).then(
(res) => res.json().then(
(data) => { console.log(data); setRepr(data.repr) }
)
)
}
return (<div>
<input type="text" value={input} onChange={(e) => setInput(e.target.value)}></input>
<button onClick={getClick}>Get</button>
<p>repr: {repr}</p>
</div>
)
}
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox />);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li>{x}</li>)}
</ol>
</div>)
}
export { GenreBox, GenreBoxList }
When I click the + and - button with index == 0,
I expect the front of the list to be modified.
However, it appears that no matter what number I set the index to,
it is always operating on the tail of the list...
What am I doing wrong or is this a bad design practice?
EDIT 1:
OK, it seems to be the problem with the key. React seems to treat objects with the same type and key to be equal and hence does not update the page.
EDIT 2:
Now I have added keys to both and and it seems to be functioning correctly. So is this how react is proposed to be used?
function GenreBoxList() {
let [genreBoxList, setGenreBoxList] = useState([])
let [index, setIndex] = useState(0)
let [counter, setCounter] = useState(0)
let insertGenreBox = (e) => {
e.preventDefault();
console.log(index);
let t = [...genreBoxList];
t.splice(index, 0, <GenreBox key={counter} ></GenreBox>);
setCounter(counter + 1);
console.log(t);
setGenreBoxList(t);
}
let removeGenreBox = (e) => {
e.preventDefault();
let t = [...genreBoxList];
t.splice(index, 1);
console.log(t);
setGenreBoxList(t);
}
let indexChange = (e) => {
e.preventDefault();
setIndex(e.target.value)
}
return (<div>
<button onClick={insertGenreBox}>+</button>
<button onClick={removeGenreBox}>-</button>
<input type='number' value={index} onChange={indexChange} />
<ol>
{genreBoxList.map((x) => <li key={x.key}>{x}</li>)}
</ol>
</div>)
}
As you realized in your edit, the key here is key.
Your list is an array of items where each item is created by the expression <GenreBox {...props} /> (which is in fact translated into React.createElement(GenreBox, props) ). When React sees an array of such, say, 10 items - it has no way to know which of them was added first. All it knows is that there are 10 of them.
For a moment, let's ignore the fact that the code later wraps each of them inside it's own <li> element, and assume we are rendering the array as-is into the <ol> container.
React sees there are 10 items of the component that should be rendered, and it invokes the rendering function for each. That function also uses state via useState() so React has to pass the correct state to each render. React looks in the state data remained from the previous render, and sees that there are 9 sets of state data since there were only 9 items in the previous render. How would it associate each set of state data to a component in the list? The only way would be to provide the first set of state data to the first item, the second set to the second item, etc. and leave the last item to initialize it's own new state.
By providing a unique key attribute, on the other hand, you are giving the item an identity. React would now be able to associate the item with the correct set of state data (and other hooks data as well) regardless of it's position in the list.
(In fact, even if you don't provide a key React would provide one, but this key would simply be the index of the item so everything said above still apply).
Lastly, since the code later maps the original array to a new array where each item is wrapped inside a <li> element, the actual relevant list is this list of <li> items, so the key should be provided there - as you indeed did.
Reference:
https://reactjs.org/docs/reconciliation.html#recursing-on-children

Why is the console.log() printing multiple times with all the previous values?

When I click the plus button, the number state updates with +1 as expected.
But if I click the plus button 5 times (Or any other number of times).
when I try to print the number value in the console with the keydown event, I get five prints of 0 to 5
(Or as many times as I previously clicked the plus button). Why is this happening? And how can I get just one print with the current (updated) value?
This is the screenshot
let [number, setNumber] = useState(0)
function plus() {setNumber(number += 1)}
function ptintNumber() {
console.log(number)
}
document.addEventListener('keydown', ptintNumber)
return <div>
<h1>{number}</h1>
<button onClick={plus}>+</button>
</div>
To answer your specific question: the reason this happens is because you tie an event listened to your document on every render, which happens each time you click the Plus button. So every keydown is calling your ptintNumber exactly as many time as you clicked the Plus button.
Solution: Put your even listener into useEffect like so:
import React from "react";
import "./styles.css";
export default function App() {
let [number, setNumber] = React.useState(0);
const number_ref = React.useRef();
number_ref.current = number;
function plus() {
setNumber((number += 1));
}
function ptintNumber() {
console.log(number_ref.current);
}
React.useEffect(() => {
document.addEventListener("keydown", ptintNumber);
}, []);
return (
<div className="App">
<h1>{number}</h1>
<button onClick={plus}>+</button>
</div>
);
}
Sandbox: https://codesandbox.io/s/wizardly-antonelli-vix6q?file=/src/App.js
You can use onKeyDown instead of addEventListener
function ButtonComp() {
let [number, setNumber] = useState(0);
// document.addEventListener("keydown", ptintNumber);
return (
<div>
<h1>{number}</h1>
<button onClick={() => setNumber(number+1)} onKeyDown={() => {console.log(number)}}>+</button>
</div>
);
}
If you wish to see the value, you need to use a useEffect hook that will react to the number change. The value you could be outputting with your method might not be the right value
import React, { useEffect, useState } from "react";
export default function App() {
let [number, setNumber] = useState(0);
function plus() {
setNumber((previousNumber) => previousNumber + 1);
}
useEffect(() => {
console.log(number);
}, [number]);
return (
<div className="App">
<h1>{number}</h1>
<button onClick={plus}>+</button>
</div>
);
}
You should also base your new value on the previous one
Codesandbox example

Find an update object from an array in react using useContext

This is a tricky question and I have been having a hard time figuring out this. First of all I'm using useContext to make a global state. The global state will hold and serve an array with objects. I have a form rendered together with every object. The form will have an input with an value.
My goal is to be able to find the object and update the value. For example the input in Item1 will update "20" to whatever new number that are being inputted, same with Item2
What is happened now I that every time I submit an input, a whole new array are being created instead on updated. I know its a whole easier way to achieve this, but I need this array to be in a global state.
here's link to my sandbox
https://codesandbox.io/s/muddy-wildflower-h5hhw?file=/src/App.js
Thanks!
You need to specify which Array Item you want to update. First of all you need to specify which item you need to update. For this i've passed the value id through the card props
<Card
name={value.name}
value={value.Itemvalue}
key={value.id}
id={value.id}
/>
And i've used map to update the specific object
const updatedData = value.map((obj) => {
if (obj.id === id) {
return { ...obj, Itemvalue: itemValue };
} else return obj;
});
updateValue(updatedData);
And here is the working Link
The problem is you're adding a new element everything the form is updated. Which is different from what you need. Pass id to the update function, so that you can update that particular item.
Your code
const addValue = (event) => {
event.preventDefault();
// You're not updating existing element, instead adding a new value.
updateValue((prevItems) => [...prevItems, { Itemvalue: itemValue }]);
};
// ...
<form onSubmit={addValue}>
<input
type="text"
name="price"
Value={itemValue} // --> Typo here
onChange={updateItemValue}
/>
<button>Submit</button>
</form>
You should pass the id as props instead of getting it from context. So <Add /> knows exactly where to update the value.
Corrected code
import React, { useContext, useState } from "react";
import { ValueContext } from "./ValueContext";
const Add = ({ id }) => { // --> Passing id from <Card />
const [itemValue, setItemValue] = useState("");
const [value, updateValue] = useContext(ValueContext);
const updateItemValue = (event) => {
setItemValue(event.target.value);
};
const addValue = (event) => {
event.preventDefault();
updateValue((prevItems) => {
const index = prevItems.findIndex(x => x.id === id);
const updatedItem = {
...prevItems[index],
Itemvalue: itemValue
}
return [
...prevItems.slice(0, index),
updatedItem, // --> Updating the item, instead of creating new one
...prevItems.slice(index + 1)
]
});
};
return (
<form onSubmit={addValue}>
<input
type="text"
name="price"
Value={itemValue}
onChange={updateItemValue}
/>
<button>Submit</button>
</form>
);
};
export default Add;

Click handler, in react, doesn't return latest change in state when it executes a 2nd time

This takes place in a functional component:
import {useEffect} from 'react';
let [clickedOnPiece, setClickedOnPiece] = useState(false);
let [testRender, setTestRender] = useState(false);
useEffect(() => {
testRenderFunction();
}, [])
function testRenderFunction() {
let el = <div onClick={onClickHandler}>Click me</div>;
setTestRender(el);
}
function onClickHandler() {
if (clickedOnPiece) {
console.log("already clicked")
return
}
console.log(clickedOnPiece); //returns false the 1st & 2nd time.
setClickedOnPiece("clicked");
}
return (
<>
{testRender}
</>
)
When I click on div for the first time, I wait until setClickedOnPiece("clicked") successfully updates clickedOnPiece to "clicked". (I check this with React Developer Tools).
When I click div the 2nd time, it doesn't log the new change in state. It still logs clickedOnPiece as false. Why is this?
Okey this problem is because useState is asyncronus. u can read more about this useState set method not reflecting change immediately.
I think the solution is add useEffect like this.
useEffect( () => {
console.log(clickOnPiece);
}
, [clickOnPiece])
If you want to toggle the state, you could do something like this:
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
// set the value of clickedOnPiece to the opposite of what it was
// i.e. if it was 'true', set it to 'false'.
setClickedOnPiece(!clickedOnPiece);
console.log(clickedOnPiece);
}
// call the onClickHandler on click
<div onClick={()=>onClickHandler()}>Click me</div>
Looks like you are toggling
let [clickedOnPiece, setClickedOnPiece] = useState(false);
const onClickHandler = () => {
console.log(clickedOnPiece);
setClickedOnPiece(!clickedOnPiece);
}
console.log(clickedOnPiece);
<div onClick={onClickHandler}>Click me</div>
After setting state, don't console immediately because state is an asynchronous.
onClickHandler references the old, previous variable, clickedOnPiece. I believe this is because onClickHandler is not defined in the return statement part of the functional component which would have allowed it a new onClickHandler body to be created each time. Instead, we have the old onClickHandler continually referencing the old clickedOnPiece.
This problem is known as 'stale closures' - a concept I found discussed well at the bottom of this article

TextField loses focus when State changes

I am dynamically creating a form according to the button clicks, so when the user presses the button, the form generates with a new id. SO, i created a state for the createform counter like this,
const [formmakersno, setFormmakersno] = useState([0]);
and inside my render, I have something like this :
{formmakersno.map((no) => {
return formDetails;
})}
My "formDetails" is a constant with jsx, it just returns the form elements like a textfield and a button. Everything works fine, Everytime i click the button the form generates as expected, But, I want to pass the counter number every-time it creates a new form, as a prop, since i cannot do this when it is just a const which returns a jsx, I converted it into an arrow function so that i can pass props and passed it like this :
{formmakersno.map((no) => {
return <formDetails value={no} />;
})}
And i could access the props too, but the problem is that , everytime I type inside the textfield for each letter the textfield goes out of focus, because its a component it renders everytime because I am calling an Onchange method with a state change inside it. Is there a workaround for this ?
EDIT : I created this simple example to further illustrate the problem I am facing :
export const CreatePush2 = () => {
const [titles, setTitles] = useState(["", ""]);
const [numbers, setNumbers] = useState([0, 1, 2]);
const handleChange = (e, id) => {
const { value } = e.target;
let titles_1 = [...titles];
let titles_11 = titles_1[id];
titles_11 = value;
setTitles(titles_1);
console.log(titles);
};
const InputCreator = ({ id }) => {
return (
<form>
<input type="text" onChange={(e) => handleChange(e, id)} value={titles[id]} />
<br />
</form>
);
};
return (
<div>
{numbers.map((number) => {
return <InputCreator key={number.toString()} id={number} />;
})}
</div>
);
};
export default CreatePush2;
The textfield loses focus with every letter typed.

Categories