I have just started learning react.
I have a component which is calling the weather API and fetching data for user provided location, user input is getting updated under userLoc but somehow the state is not getting updated for finalLoc and whenever I console log it, it is showing undefined.
const Inputs = props => {
const [userLocation, setUserLocation] = React.useState('')
const [finalLocation, setFinalLocation] = React.useState('')
function fetchLocation(e) {
setUserLocation (e.target.value)
}
function fetchDetails(e) {
e.preventDefault()
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
const API_KEY = '&API_KEY'
let total = baseURL + userLocation + API_KEY
console.log(userLocation) // Outputs the input value
setFinalLocation(userLocation)
console.log(finalLocation) // Comes as blank
console.log(total);
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={loc.userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={loc.finalLoc} results={loc.result} />
</div>
)
}
const Inputs = props => {
const [finalLoc, setFinalLoc] = React.useState("");
const [userLoc, setUserLoc] = React.useState("");
const [data, setData] = React.useState([]);
function fetchLocation(e) {
userLoc( e.target.value )
}
function fetchDetails(e) {
let baseURL = '//api.openweathermap.org/data/2.5/weather?q='
let API_KEY = '&appid=API_KEY'
let total = baseURL + loc.userLoc + API_KEY
setFinalLoc( userLoc )
fetch(total)
.then(response => response.json())
.then(data => {
setData( data )
})
}
return (
<div className='inputs'>
<p className="label">Enter the location to find the weather.</p>
<input type="text" className='loc-input' autoFocus placeholder='Enter a location' name="" id="location" onChange={fetchLocation} value={ userLoc || ""} />
<button onClick={fetchDetails}>Get Details</button>
<Outputs loc={ finalLoc} results={data} />
</div>
)
}
If you update a state when using useState you don't write it as
setState({state: //data})
But
setState(//data)
The updating method in useState is similar to class component setState method but you have to update it differently.
See more about useState here.
If your state is an object you should update it as an object:
const [state, setState] = useState({
name: "John",
lastName: "Due",
});
setState({
name: "Liam",
lastName: "call",
});
BTW,
If you are updating multiple values in your state, You should update it in one function.#1
Note:
Each time you're updating your state cuase React to re-render and slow your app.
For better performance you should always try to re-render your app as less as possible.
Edit:
#1 if your state is an object you should update it in one function.
You can use the functional form. The function will receive the previous value, and return an updated value. so to update the state we have to make copy of previous value with updated one and then set the state with updated values
setLoc((loc) => ({ ...loc, finalLoc: loc.userLoc }));
same when you get result data keep copy of previous state
setLoc((loc) => ({ ...loc, result: data }));
Related
I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here
I have a search component that fetches a single profile from a JSON file (currently local, but will be remote in the future) and displays the information of the matching profile beneath the input field.
Currently, on my first submit of my search query, I've found that all of my state variables return undefined because, if I understand correctly, state does not update until the full chain of promises has resolved. And it's only on my second submit of my search query that my state variables return the correct data of the filtered search result.
On the first submit, it appears that an empty array is being initialized, as my conditional render of {props.activeChart && `OPENED CHART : ${props.firstName} ${props.lastName} (DOB: ${props.DOB})`} becomes truthy and renders out empty values for firstName, lastName, and DOB.
EDIT: I came across this recent post (React state gets updated only after I submit the form twice), which seems to address this same issue resulting from asynchronous fetch and setting state, except with axios. I've tried modifying my code accordingly (edited below), but I'm still not able to update state after my fetch result has resolved. Any advice would be appreciated. Thanks.
import { useState } from 'react';
import StyledSearchForm from './SearchForm.styled';
const SearchForm = props => {
const [queryFirstName, setQueryFirstName] = useState('');
const [queryLastName, setQueryLastName] = useState('');
const [queryDOB, setQueryDOB] = useState('');
const handleQuery = async (e) => {
e.preventDefault();
const result = await fetchRecord();
console.log(result[0]) // fetched object successfully logged
if (result[0]) {
setActiveChart(result[0]);
console.log(activeChart) // activeChart still undefined
setFirstName(activeChart.firstName);
setLastName(activeChart.lastName);
setDOB(activeChart.dob);
}
};
const fetchRecord = () => (
fetch('http://localhost:8000/patients')
.then(resp => { return resp.json(); })
.then(data => {
const result = data.filter(patient => (
(patient.dob === queryDOB.trim() &&
patient.lastName.toLowerCase() ===
queryLastName.toLowerCase().trim()) ||
(patient.lastName.toLowerCase() ===
queryLastName.toLowerCase().trim() &&
patient.firstName.toLowerCase() ===
queryFirstName.toLowerCase().trim())
));
return {...result};
})
);
return (
<StyledSearchForm>
<form onSubmit={handleQuery}>
<label className="first-name" htmlFor="first-name">
First Name:
</label>
<input
type="text"
id="first-name"
className="form-fields"
name="fname"
value={queryFirstName}
onChange={e => setQueryFirstName(e.target.value)}
/>
<label className="last-name" htmlFor="last-name">
Last Name:
</label>
<input
type="text"
id="last-name"
className="form-fields"
name="lname"
value={queryLastName}
onChange={e => setQueryLastName(e.target.value)}
/>
<label className="dob" htmlFor="dob">
DOB:
</label>
<input
type="text"
id="dob"
className="form-fields"
name="dob"
value={queryDOB}
onChange={e => setQueryDOB(e.target.value)}
/>
<button className="submit-btn" type="submit" onClick={e => handleQuery}>Open Chart</button>
</form>
<div className="active-patient">
{props.activeChart && `OPENED CHART : ${props.firstName} ${props.lastName} (DOB: ${props.DOB})`}
</div>
</StyledSearchForm>
);
};
export default SearchForm;
It looks like you're expecting your data filter to return an object, but Array.prototype.filter (docs) returns an array. Arrays, even if empty, are truthy.
You need to handle an array, not an object, in this chain:
const fetchRecord = () =>
fetch("http://localhost:8000/patients")
.then((resp) => {
return resp.json();
})
.then((data) => {
// results is an array!
const results = data.filter(...);
if (results.length === 0) {
// no match - do something about it?
return {};
} else {
// return the first result?
return results[0];
}
})
.then((record) => {
props.setActiveChart(...record);
})
.then(() => {
props.setFirstName(props.activeChart.firstName);
props.setLastName(props.activeChart.lastName);
props.setDOB(props.activeChart.dob);
});
It seems the issue resulted from trying to set all of my state variables in the same async function that was fetching my search result, and by moving the if(results[0]) statement out of the handleQuery function while leaving just the setActiveChart() inside the handleQuery function resolved my issue.
I'm updating one of my state properties without using the setter and using a normal variable assignment in js, that causes me the problem that the property state only updates once, so when i want to check a checkbox more than once the state in app does not update correctly, i think that the problem causing it is that i'm messing up that property assignment.
I searched for how to update a property of a state without overwritting the whole object and every post i find answering that question is using this.setState instead of declaring a state and using the declared setter method and i don't know how to adapt that this update to my code.
For example, the top post called "How to update nested state properties in React" declares his state like this:
this.state = {
someProperty: {
flag:true
}
}
While i'm declaring my state like this:
const [EditedTask, setEditedTask] = useState({
name: props.task.name,
completed: props.task.completed,
_id: props.task._id,
});
const { name, completed } = EditedTask;
And the top answer to that question is:
this.setState(prevState => ({
...prevState,
someProperty: {
...prevState.someProperty,
someOtherProperty: {
...prevState.someProperty.someOtherProperty,
anotherProperty: {
...prevState.someProperty.someOtherProperty.anotherProperty,
flag: false
}
}
}
}))
In this solution, with which object should i replace the this in the this.setState? Or the prevState param?
In my code i need to update the checkbox/ completed state, with this solution i'm only receiving the object EditedTask in my app component when i edit the checkbox once, if i check it more than once the state doesn't update in app, meanwhile if i edit the name the state updates in app correctly with both the name and completed propertys.
import React, { useState } from "react";
import "../App.css";
const EditTask = (props) => {
const [EditedTask, setEditedTask] = useState({
name: props.task.name,
completed: props.task.completed,
_id: props.task._id,
});
const { name, completed } = EditedTask;
const onChange = (e) => {
setEditedTask({
...EditedTask,
[e.target.name]: e.target.value,
});
};
const onSubmit = (e) => {
e.preventDefault();
setEditedTask(EditedTask);
props.saveEditedTask(EditedTask);
props.changeEdit(false);
};
return (
<>
<form onSubmit={onSubmit}>
<div className="form-group">
<label>Task Id: ${props.task._id}</label>
<div className="inputEdit">
<input
type="text"
placeholder={props.msg}
name="name"
onChange={onChange}
value={name}
/>
</div>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
defaultChecked={completed}
value={completed}
onChange={() => (EditedTask.completed = !EditedTask.completed)}
name="completed"
/>
<label className="form-check-label">Completed</label>
</div>
<div className="submit">
<button type="submit">Edit</button>
</div>
</form>
</>
);
};
export default EditTask;
I tried replacing the onChange={() => (EditedTask.completed = !EditedTask.completed)} like the top answer/ my onChange for the name value with something like this but it doesn't update the completed value
const updateCompleted = (e) => {
setEditedTask({
...EditedTask,
[e.target.name]: !e.target.value,
});
};
You can try something like this in your setter functions.
const updateCompleted = (e) => {
setEditedTask((currentState) => ({
...currentState,
[e.target.name]: !e.target.value
})
)};
This solution will give you your previous state, which your can use to update your state.
I have a code like this
const ChildComponent = ({ products, setProducts }) => (
<form>
<input type="text" value={products.name} onChange={(e) => setProducts(e.target.value)} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState(
{
id: 1,
name: "Test",
}
);
useEffect(() => {
// Where i call API to get list product and set it to child component
}, [])
return <ChildComponent products={products} setProducts={setProducts} />
}
for some reason , i can ONLY update state of ParentComponent in ChildComponent. It's work but i think it's so weird, that look like i change props of child component everytime when i make a edit of input. Can any one tell me that is an anti pattern or not.Sorry about my bad English. Thank you so much!
It's not an anti-pattern to pass the state object and state updater function as props, but this offloads the responsibility to update state correctly and maintain the state invariant to consuming components.
As you can see, your child component already messes up and changes the state shape/invariant from Object to String.
const [products, setProducts] = useState({ // <-- object
id: 1,
name: "Test",
});
... child
onChange={(e) => setProducts(e.target.value)} // <-- string value
On the subsequent render attempting to access value={products.name} in the child will fail as now products is a string.
I typically suggest declaring a handler function to do the state update and pass that instead.
In your snippet it seems the child component is more a "controlled input" meaning it's an input tag with a value and onChange handler. This is an example refactor I would do.
const ChildComponent = ({ value, onChange, onSubmit }) => (
<form onSubmit={onSubmit}>
<input type="text" value={value} onChange={onChange} />
<input type="submit" value="Finish" />
</form>
)
const ParentComponent = () => {
const [products, setProducts] = useState({
id: 1,
name: "Test",
});
const changeHandler = e => {
setProducts(products => ({
...products,
name: e.target.value,
}));
};
const onSubmit = e => {
e.preventDefault();
// handle the form submission
};
useEffect(() => {
// Where i call API to get list product and set it to child component
}, []);
return (
<ChildComponent
value={products}
onChange={changeHandler}
onSubmit={submitHandler}
/>
);
}
This way the parent maintains control over both the state updates and how the form data is submitted. The child hasn't any idea what the value represents and it isn't trying to update anything in any way, but simply passing back out the events.
In my app I have profile section with a form. When the component mounts I want to fetch user data from firebase, and display it in the form, with the current values of the user profile. Either using the "value" prop or the "placeholder" prop.
When the user makes changes in the form inputs and submit the changes, I want the database to update and the form to update with the new data.
Currently I can make the database value appear in the form input field, or I can make the form input field empty, but update the database. But not both.
The following code makes the database data render in the form input, but it cant be changed.
I know it could be something with the second useEffect() and the getUserData() function, that I cant seem to figure out.
const UserEdit = (props) => {
const [currentUser, setCurrentUser] = useState('');
const [forening, setForening] = useState('');
useEffect(() => {
firebase_app.auth().onAuthStateChanged(setCurrentUser);
}, [])
const getUserData = async () => {
await dbRef.ref('/' + currentUser.uid + '/profil/' ).once('value', snapshot => {
const value = snapshot.val();
setForening(value)
})
}
useEffect(() => {
getUserData()
},[] )
const handleInput = (event) => {
setForening(event.target.value)
}
const updateUserData = () => {
dbRef.ref('/' + currentUser.uid + '/profil/' ).set({foreningsnavn: forening}, function(error) {
if(error) {
console.log("update failed")
} else {
alert(forening)
}
})
}
const handleClick = () => {
updateUserData()
}
return (
<>
<div className="card-body">
<div className="row">
<div className="col-md-5">
<div className="form-group">
<label className="form-label">{Forening}</label>
<input className="form-control" type="text" value={forening} onChange={handleInput}/>
</div>
</div>
</div>
</div>
</>
)
}
Your second useEffect will run only one time because the second argument array [] of dependencies is empty:
useEffect(() => {
getUserData()
},[] )
You can add foreign dependency to make useEffect run with input change
useEffect(() => {
getUserData()
},[foreign] )
or you can use polling to sync database state