In the following code, the hook shown below does not change the state of the variable newName.
import React, { useState } from 'react'
const App = () => {
const [ persons, setPersons] = useState([
{ name: 'Arto Hellas' }
])
const [ newName, setNewName ] = useState('')
const textChangeHandler = (event)=>{
event.preventDefault()
setNewName(event.target.value) // WORKS FINE
}
const submitHandler = (event)=>{
event.preventDefault()
let temp = {name:newName}
setNewName('') //////////////////////////////////////// PROBLEM - doesnot set state!!
console.log('tenp name is',temp.name)
console.log('new name is',newName)
setInterval(()=>console.log("Set Interval",newName), 1000)
}
return (
<div>
<h2>Phonebook</h2>
<form onSubmit={submitHandler}>
<div>
name: <input onChange={textChangeHandler} />
</div>
<div>
<button type="submit" >add</button>
</div>
</form>
<h2>Numbers</h2>
{persons.map((person) => <Person key = {person.name} name={person.name}/> )}
</div>
)
}
const Person = ({name})=> <p> {name} </p>
setNewName works fine and the name is updated when anything is typed in the input box. However, when I submit button, the setNewName does not seem to work. Even after updating, executing the setNewName the name is still the old name.
I have even tried a setInterval (thinking it may be due to asynchronous nature of JS) and printed the newName but, it still shows the old name.
What is the problem and how can it be fixed?
thanks
If you look at your submitHandler function, you will notice that the name its self has not changed... it remains empty as in its initial state. React only reload if the state value changes as shown below
const submitHandler = (event)=>{
event.preventDefault()
let temp = {name:newName}
//setNewName('') // your value is not changing here ... removing line
setNewName(temp.name) // changing the name here with the one in temp
console.log('tenp name is',temp.name)
console.log('new name is',newName)
setInterval(()=>console.log("Set Interval",newName), 1000)
}
As you know setting a state is asynchronous. Never use scheduling functions like setTimeout setInterval to log out the value of state.
Use useEffect instead -
useEffect(() => {
console.log("value is", newName);
}, [newName]);
You hook is working fine without a problem. Here's a codeSandbox showing everything's working fine.
In React setState is not synchronous ( with class components and hooks ).
So you can not expect that a new state will be available immediately.
You need to change the implementation of the submitHandler function.
Related
I try to use watch hook to watch a variable changes - city. However, I would like that only city variable produces the effect, not all the form (say, name should not log anything): see the sample on Stackblitz:
import * as React from 'react';
import { useForm } from 'react-hook-form';
export default function App() {
const { watch, register } = useForm({
mode: 'all',
reValidateMode: 'onBlur',
});
const cityValue = watch('city', 'my city');
const nameValue = watch('name', 'my name');
// Will console the changes
React.useEffect(() => {
const subscription = watch((value, { name, type }) =>
console.log(value, name, type)
);
return () => subscription.unsubscribe();
}, [cityValue]);
return (
<div>
<p>Start editing & look at the console how values changes :</p>
<input value={cityValue} {...register('city')} />
<label>{cityValue}</label> <br />
<input value={nameValue} {...register('name')} />
<label>{nameValue}</label> <br />
</div>
);
}
Why does the [cityValue] filter in the useEffect not work?
It seem that the useEffect with [cityValue] above would be same as useEffect with []...
The condition on useEffect only states under which circumstances to re-run the effect, it has no effect on the values inside the effect. The watch call inside your effect applies to all watches it seems, and registers a change-listener for all of them. It also does so every time the city value changes, which you probably don't want.
I think what you want is just:
React.useEffect(() => console.log(cityValue), [cityValue]);
I understand (somewhat) how to use useEffect to update state, but I struggle with situations like when you need current state inside of another function, before the "nextTick" as it were.
Here is a simple Codepen with the exact issue. Make sure the Pen console is open.
https://codepen.io/kirkbross/pen/vYRNpqG?editors=1111
const App = () => {
const [state, setState] = React.useState(null);
// how can I make sure the below function knows what the current state really is?
const handleAppend = (state) => {
console.log("click");
console.log(state?.text + " foobar");
};
return (
<div class="app">
<div className="row">
<span>Text: </span>
<input
type="text"
onChange={() => setState({ text: e.target.value })}
/>
</div>
<div className="row">
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
</div>
);
};
You're shadowing your state variable in your handleAppend function. You don't need to pass in an argument since state is available in scope of the component
const handleAppend = () => {
console.log("click");
console.log(state?.text + " foobar");
};
I did some changes. You dont need to use ur state as a parameter, since your textState lives inside your app component and there for you can reach it within your function.
Also, i changed the state and setState to textState, setTextState to make it less confusing. Also after clicking on the button and console logging, i cleared the textState so the next value wont be effected. Check it out below.
function App() {
const [textState, setTextState] = React.useState(null);
const handleAppend = () => {
console.log("click");
console.log(textState + " foobar");
setTextState('')
//also, you could make the input box clear after each press on button by adding value={textState} in the input.
};
return (
<div className="App">
<input
type="text"
onChange={(e) => setTextState(e.target.value)}
/>
<button onClick={handleAppend}>
Append "foobar" to text and log it to console
</button>
</div>
);
}
My real world case was more complicated than the Pen. The actual function needing state was a useCallback function and I had forgotten to add state to the dep array of the useCallback function.
const handleDragEnd = useCallback(
async (result) => {
const { source, destination, draggableId } = result;
console.log(state); // shows up now.
},
[state], // I had forgotten to add state to the useCallback dep array
);
I am trying to build a functionality where when a user navigates away from the form i.e when component unmounts it should trigger a save i.e post form data to server. This should happen only if there is any change in form data. Can anyone guide me as to why this is happening. I have tried class based approach which works but I do not want to refactor my production code.
import { useCallback, useEffect, useState } from "react";
import React from "react";
import * as _ from "lodash";
import { useFormik } from "formik";
// for now this is hardcoded here..but let's assume
// this server data will be loaded when component mounts
const serverData = {
choice: "yes",
comment: "some existing comment"
};
const availableChoices = ["yes", "no"];
const Form = () => {
const formik = useFormik({ initialValues: { ...serverData } });
const [isFormChanged, setIsFormChanged] = useState(false);
const valuesHaveChanged = React.memo(() => {
console.log("INIT VALUES= ", formik.initialValues);
console.log("FINAL VALUES = ", formik.values);
return !_.isEqual(formik.initialValues, formik.values);
}, [formik.initialValues, formik.values]);
const triggerSave = () => console.log("Save");
useEffect(() => {
// setForm({ ...serverData });
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (setIsFormChanged) {
triggerSave();
}
};
});
return (
<form>
<div className="form-group">
{availableChoices.map((choice) => (
<label key={choice}>
{choice}
<input
id="choice"
value={choice}
className="form-control"
type="radio"
name="choice"
checked={choice === formik.values.choice}
onChange={formik.handleChange}
/>
</label>
))}
</div>
<div className="form-group">
<textarea
rows="5"
cols="30"
id="comment"
name="comment"
value={formik.values.comment}
onChange={formik.handleChange}
className="form-control"
placeholder="some text..."
></textarea>
</div>
</form>
);
};
export default Form;
The first problem i spotted is the dependency array.
useEffect(() => {
// the flag can be set anytime upon a field has changed
// maybe formik has a value like that, read doc
if (valuesHaveChanged) {
setIsFormChanged(true);
}
return () => {
if (setIsFormChanged) {
triggerSave();
}
}
// the dependency array is [], can't be missed
}, [])
Currently you are calling this effect and cleanup this effect in every update, ex. if any value changes in this component. But normally you only want to do it once upon dismount.
Even you do the above right, you still need to make sure your code contains no memory leak, because you are trying to do something upon the dismount. So it's better to pass the values:
triggerSave([...formik.values])
And make sure inside triggerSave, you don't accidently call anything about formik or setState.
Try to use useEffect with dependencies
useEffect(() => {
return () => {
// when this cleanup function runs
// i.e when this component unmounts,
// i need to check if there
// was any change in the form state
// if there was a change i need to trigger a save
// i.e post form data to server.
if (!_.isEqual(formik.initialValues, formik.values)) {
triggerSave();
}
};
}, [formik.values]); // won't run on every render but just on formik.values update
Explanation:
useEffect has dependencies as a second argument, if [] is passed - effect is triggered only on mount, if [...] passed, will trigger on the first mount and on any of ... update.
If you don't pass the second agrument, useEffect works as a on-every-render effect.
The user must enter a name and hospital in the form, then their name and hospital will appear in a side menu. But I can't make it happen, useState returns an empty state so it's not working
export function Home() {
const [nome, setNome] = useState("")
const [hospital, setHospital] = useState("")
const [describe, setDescribe] = useState("")
const [patient, setNewPatient] = useState({})
function handleCreateNewPatient(event){
event.preventDefault()
setNewPatient({
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
})
Users.push(patient)
}
the code with UI material of the form
<form onSubmit={handleCreateNewPatient} >
<div>
<TextField
value={nome}
onChange={event=>setNome(event.target.value)}
/>
<TextField
value={hospital}
onChange={event=>setHospital(event.target.value)}
/>
</div>
<div>
<TextareaAutosize
value={describe}
onChange={event=>setDescribe(event.target.value)}
/>
</div>
<Button type="submit">Confirmar</Button>
</form>
The issue is here:
setNewPatient({
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
})
Users.push(patient)
setState() is asynchronous. React does not guarantee that the state changes are applied immediately. Think of setState() as a request rather than an immediate command to update the component.
To get the updated value you have to use useEffect like:
useEffect(() => {
if( patient.hasOwnProperty('id') ) // Checking if patient is set or not
{
Users.push(patient)
}
}, [patient]); // Here `patient` is the dependency so this effect will run every time when `patient` change.
Such type of logic will help you.
State updates are not synchronous in React. You can do something like this:
function handleCreateNewPatient(event){
event.preventDefault();
const patient = {
id: nome.length,
nome: nome,
hospital: hospital,
describe: describe
};
setNewPatient(patient)
Users.push(patient)
}
or write an effect as mentioned in the other answer.
This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}