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.
Related
In my project, I had retrieved a list of arrays from firestore database in UseEffect(). After that, I had filtered the list of arrays into a single array called medicineStock and passing each value to new React useStates (editMedicineName, editMedicineRegNo, and editMedicineType) and making them as values in my input, however it doesn't pass anything into each useStates and my input field stays empty.
function EditMedicine() {
const { medicineId } = useParams();
const [medicine, setMedicine] = useState([]);
useEffect(() => {
const q = query(collection(db, 'medicine'), where("isAccepted", "==", true) )
onSnapshot(q, (querySnapshot) => {
setMedicine(querySnapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
})))
})
},[])
const medicineStock = medicine.filter(medicineStock => medicineStock.id === medicineId);
const [editMedicineName, setEditMedicineName] = useState(medicineStock.map(item => item.data.medicineName).toString());
const [editMedicineRegNo, setEditMedicineRegNo] = useState(medicineStock.map(item => item.data.medicineRegNo).toString());
const [editMedicineType, setEditMedicineType] = useState(medicineStock.map(item => item.data.medicineType).toString());
return (
<div className='editMedicine'>
<div className="editMedicine-title">Edit Medicine</div>
<form className="editMedicine-form" >
<div>
<p>
<label className="editMedicine-dataTitle">Name: </label>
<input
name="medicineName"
type="text"
required
className="editMedicine-input"
value={editMedicineName}
onChange={(e) => setEditMedicineName(e.target.value)}
/>
</p>
<p>
<label className="editMedicine-dataTitle">Reg No: </label>
<input
name="medicineRegNo"
type="text"
required
className="editMedicine-input"
value={editMedicineRegNo}
onChange={(e) => setEditMedicineRegNo(e.target.value)}
/>
</p>
<p>
<label className="editMedicine-dataTitle">Type: </label>
<select className="editMedicine-input" value={editMedicineType} onChange={(e) => setEditMedicineType(e.target.value)}>
<option value="Tablet">Tablet</option>
<option value="Capsule">Capsule</option>
<option value="Liquid">Liquid</option>
<option value="Spray">Spray</option>
<option value="Cream">Cream</option>
<option value="Ointment">Ointment</option>
</select>
</p>
</div>
</form>
</div>
)
}
I tired to console.log the medicineStock which contain the filtered single array in the medicine object and console return this:
[]
length: 0
[[Prototype]]: Array(0)
[{…}]
0:
data:
donationId: "XOCXD3FFaWGjO3oZcJmc"
medicineName: "Fluticasone Nasal Spray BP 50 mcg"
medicineRegNo: "MAL14125126AZ"
medicineType: "Spray"
[[Prototype]]: Object
id: "Ko6KqA9nyIotUVOkmfPL"
[[Prototype]]: Object
length: 1
[[Prototype]]: Array(0)
Where the first console.log return no value, while second console.log return the value I want to put into the React usestates. Is there any solution to solve this issue?
There is few misunderstoods about how useState works in your code and generally how you can trigger a rerendering.
What is happening in your code:
You init medicine state property with an empty array -> OK
You declare a new constant medicineStock which is a filtered version of medicine. At this moment medicineStock is an empty array too.
You init 3 new states properties with an initialValue based on medicineStock. It will produce 3 new empty strings. Here you have to understand that the initialValue of a state is not designed to update it but only to init it. If you update medicineStock, it will not update editMedicineName for example.
Then you fetch some data and call setMedicine to store it. This will have for effect to trigger a new rendering. So the body of your function will be read again. Of course medicineStock will have a new value a this moment and that's why you are able to see it with your log. But as I said before, this will not update editMedicineName, editMedicineRegNo and editMedicineType. The only way to update these values is to call their owns setters.
How to fix it:
I don't really understand what you are trying to achieve here but a simple fix for your code would be:
const [editMedicineName, setEditMedicineName] = useState();
const [editMedicineRegNo, setEditMedicineRegNo] = useState();
const [editMedicineType, setEditMedicineType] = useState();
useEffect(() => {
const q = query(collection(db, 'medicine'), where("isAccepted", "==", true) )
onSnapshot(q, (querySnapshot) => {
const medecine = querySnapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
}));
const medicineStock = medicine.filter(medicineStock => medicineStock.id === medicineId);
setEditMedicineName(medicineStock.map(item => item.data.medicineName).toString());
setEditMedicineRegNo(medicineStock.map(item => item.data.medicineRegNo).toString());
setEditMedicineType(medicineStock.map(item => item.data.medicineType).toString());
})
},[])
I'll highlight problems and solutions related to your code:
When you try to initialize your inputs useState, the medicine array is not ready yet, since the firestore call is asynchronous, so when medicine array arrives, the inputs useState (editMedicinName...) initial values will be stuck to an empty array.
You also have some problems in your data manipulation since you are placing a type Array inside your editMedicine**** states, but then in your inputs onChange handlers you are passing strings: setEditMedicineName(e.target.value)}
The solution to the first problem is to work with React hook useEffects.
The first useEffect is executed only once on mount, it calla firestore db, retrieves medicines list, and sets it to your react state medicine.
When medicine is updated, the second useEffect will be triggered, and inside of it you can update your input base values.
The second problem is addressed by setting directly strings values as your input base values.
Last but not least, I expect you don't want to show your form until you don't
receive medicine data, so it's a good idea to place a ternary
operator after the return, so you can show a spinner during loading
operations.
function EditMedicine() {
const { medicineId } = useParams();
const [medicine, setMedicine] = useState([]);
useEffect(() => {
const q = query(
collection(db, 'medicine'),
where('isAccepted', '==', true)
);
onSnapshot(q, (querySnapshot) => {
// setMedicine is called asynchronously
setMedicine(
querySnapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}))
);
});
}, []);
// Filter medicine array, when it will be retrieved from firestore
const medicineStock = useMemo(
() =>
medicine.filter((medicineStock) => medicineStock.id === medicineId)[0]
.data,
[medicine]
);
const [editMedicineName, setEditMedicineName] = useState(null);
const [editMedicineRegNo, setEditMedicineRegNo] = useState(null);
const [editMedicineType, setEditMedicineType] = useState(null);
/* Set editMedicineName, editMedicineRegNo and editMedicineType when medicine array is retrieved */
useEffect(() => {
if (medicine) {
setEditMedicineName(medicineStock.medicineName);
setEditMedicineRegNo(medicineStock.medicineRegNo);
setEditMedicineType(medicineStock.medicinType);
}
}, [medicine]);
return !medicine ? "Loading..." :(
<div className="editMedicine">
<div className="editMedicine-title">Edit Medicine</div>
<form className="editMedicine-form">
<div>
<p>
<label className="editMedicine-dataTitle">
Name:
</label>
<input
name="medicineName"
type="text"
required
className="editMedicine-input"
value={editMedicineName}
onChange={(e) => setEditMedicineName(e.target.value)}
/>
</p>
<p>
<label className="editMedicine-dataTitle">
Reg No:
</label>
<input
name="medicineRegNo"
type="text"
required
className="editMedicine-input"
value={editMedicineRegNo}
onChange={(e) => setEditMedicineRegNo(e.target.value)}
/>
</p>
<p>
<label className="editMedicine-dataTitle">
Type:
</label>
<select
className="editMedicine-input"
value={editMedicineType}
onChange={(e) => setEditMedicineType(e.target.value)}
>
<option value="Tablet">Tablet</option>
<option value="Capsule">Capsule</option>
<option value="Liquid">Liquid</option>
<option value="Spray">Spray</option>
<option value="Cream">Cream</option>
<option value="Ointment">Ointment</option>
</select>
</p>
</div>
</form>
</div>
);
}
I am building an app where the flask rest API takes two strings and gives a floating value as a prediction. Now I am trying to connect to the react app so that the predictions can be shown on a webpage.
Goal: takes two strings from the frontend and does inference using restapi and gives the values of the prediction on the front end.
Below is the code used to fetch the rest API predictions in react app.
function App() {
const [state, setState] = useState([{}]);
useEffect(() => {
fetch("/predict?solute=CC(C)(C)Br&solvent=CC(C)(C)O")
.then((res) => res.json())
.then((data) => {
setState(data);
console.log(data);
});
}, []);
In fetch /predict?solute=CC(C)(C)Br&solvent=CC(C)(C)O here solute=CC(C)(C)Br and solvent=CC(C)(C)O are the inputs for the flask rest API to give predictions.
But I want to give it from the frontend rather than mentioned in the URL. How to do it?
Modified code to fetch results and display
function App() {
const [state, setState] = useState([{}]);
const [uri, setUri] = useState([{}]);
const [resultstate, setResultState] = useState([{}]);
function HandleSubmit() {
const uri = "http://127.0.0.1:3000/?${form.one}&${form.two}";
setUri(uri);
useEffect(() => {
fetch(uri)
.then((response) => {
if (response.status === 200) {
return response.json();
}
})
.then((data) => {
setResultState(data);
console.log(data);
});
});
}
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === "INPUT") {
setState({ ...FormData, [name]: value });
}
}
return (
<div className="App">
<state onChange={handleChange}>
<fieldset>
<legend>Solute</legend>
<input name="one" value={state.one} />
</fieldset>
<fieldset>
<legend>Solvent</legend>
<input name="two" value={state.two} />
</fieldset>
<button type="button" onClick={HandleSubmit}>
Submit
</button>
</state>
<Deploy />
</div>
);
}
Running the modified code I am getting this error
Create a new form state.
Create a form with some input elements, and a button to submit the form.
When the input elements are changed update the form state with their values.
When the form is submitted create a new URI with the information in form, and then do the fetch.
const { useState } = React;
function Example() {
const [ form, setForm ] = useState({});
function handleSubmit() {
const uri = `http://example.com/?${form.one}&${form.two}`;
console.log(`Current state: ${JSON.stringify(form)}`);
console.log(`Fetch URI: ${uri}`);
// fetch(uri)... etc
}
// Because the listener is attached to the form element
// (see "Additional documentation" below)
// check that the element that's been changed is an input
// and then update the state object using the name as a key, and
// adding the value of the input as the object value for that key
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === 'INPUT') {
setForm({ ...form, [name]: value });
}
}
// Each input has a name, and maintains its
// value from the form state
return (
<form onChange={handleChange}>
<fieldset>
<legend>Input one</legend>
<input name="one" value={form.one} />
</fieldset>
<fieldset>
<legend>Input two</legend>
<input name="two" value={form.two} />
</fieldset>
<button type="button" onClick={handleSubmit}>Submit</button>
</form>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Additional documentation
Event delegation - that's what's happening when we attach one listener to the form rather than listeners to all the inputs. The form catches the events from the inputs as they bubble up the DOM. But it's also why we need to identify what element they are, hence the need for nodeName.
Destructuring assignment
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 }));
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
I am loading data on the initial load. When they user clicks the button to add a recognition, the api call adds it, and returns it. I add the new post to the array, but the new update doesn't render. The return object, and the array of objects are the same object type. When I reload the page, the new post is rendered, just not on the add function. Is there something that I am missing?
const [recognitions, setRecognitions] = useState([]);
useEffect(() => {
Api.GetRecognitions(params)
.then(response => {
const items = response || [];
setRecognitions(recognitions => [...recognitions, ...items]);
})
}, [setRecognitions]);
const handleAddPost = () => {
Api.AddRecognition(params)
.then(response => {
const newPost = response;
setRecognitions(recognitions=> [...recognitions, newPost])
});
}
<Form.Group>
<Form.Field>
<Button basic color='blue' onClick={handleAddPost}>Add</Button>
</Form.Field>
</Form.Group>
<Form.Group>
<Form.Field>
{recognitions.map(recognition => (
<RecogWallPost
key={recognition.recogStagingId}
recognition={recognition}
participantId={participantId}
/>
)
)}
</Form.Field>
</Form.Group>
Instead of passing [setRecognitions] as the second argument to useEffect, you want to pass [recognitions]. This tells the useEffect hook to run every time recognitions changes, which it does inside handleAddPost.
You have to create an async function, and then use it as follow:
useEffect(() => {
async function initData() {
Api.GetRecognitions(params)
.then(response => {
const items = response || [];
setRecognitions(recognitions => [...recognitions, ...items]);
})
}
initData()
}, [setRecognitions]);