I am learning ReactJS and trying to update the parent props with the updated state of ingredients from the child component. The setUserIngredients is called and updated ingredients are being passed to parent.
Code :
const [userIngredients, setUserIngredients] = useState([]);
const removeIngredientHandler = id => {
setLoading(true);
fetch(`https://***************.com/ingredients/${id}.json`,{
method:'DELETE'
}).then(response=>{
setLoading(false);
setUserIngredients(prevIngredients =>
prevIngredients.filter(ingredient =>{
return (ingredient.id !== id)
//return ingredient;
})
);
**props.ingredients(userIngredients);**
//userIngredients is still having old value
//need to check on this
}).catch(error => {
setError(error.message);
})
};
The problem is that userIngredients is a variable that is created when the component renders, set to a version fo the state when that component renders. And when you start an asynchronous operation (like a fetch) the callback you pass to that operation will be bound the values from when that callback was created.
The fix here is pretty simple. In the spot where you calculate your new ingredients, simply execute whatever callback you need before returning the value to be stored in the state.
Something like:
fetch(`https://***************.com/ingredients/${id}.json`, {
method: 'DELETE',
}).then(response => {
setLoading(false)
setUserIngredients(prevIngredients => {
// Figure out the new ingredients
const newIngredients = prevIngredients.filter(ingredient => ingredient.id !== id)
// Call your callback with the new ingredients
props.ingredients(newIngredients)
// Return the new ingredients to be stored in your state
return newIngredients
})
})
Related
This question already has answers here:
How to pass async state to child component props?
(2 answers)
Closed 10 days ago.
I've built a React app to search recordings, that has the following structure:
SearchScreen => SearchParms
=> SearchResults
SearchScreen is rendered like this:
return(
<Fragment>
<div className='containerCR'><SearchParms getRecordingsHandle = {this.handleGetRecordings} />
<SearchResults recordings = {this.state.recordings}/>
</Fragment>)
A button on the Search Parms screen calls this.handleGetRecordings, which calls an async function to get records, adds them to state.recordings and then passes them to the SearchResults screen.
Problem is, because it's async it passes the recordings object before it's populated.
handleGetRecordings sets the state for the search parameter ani. It needs to be done in a then because it doesn't set it immediately:
handleGetRecordings = (ani) =>
{
console.log(this.state.ani);
console.log("Passed value: ", ani);
this.setState({
ani:ani
}, () => {
console.log("Set state finished:", this.state.ani);
this.fetchRecordings();
});
}
The fetchRecordings function gets the data and then calls setRecordings:
fetchRecordings = async () => {
console.log("getting recordings for: ", this.state.ani);
const myInit = {
headers: {},
response: true,
queryStringParameters: {
ani: encodeURIComponent(this.state.ani)
}
};
console.log("Params", myInit);
API.get(myAPI, path, myInit)
.then(response => {
console.log("Response:", response)
let newRecordings = this.state.recordings;
newRecordings.push(response)
this.setRecordings(newRecordings[0].data)
})
.catch(error => {
console.log(error)
})
}
setRecordings writes the list of recordings to the state, ready to pass to the SearchResults page, except by the time it gets here its missed its chance!
setRecordings =(recordings) =>
{
console.log("Passed value: ", recordings);
this.setState({
recordings:recordings
}, () => {
console.log("Current State: ", this.state.recordings);
});
}
What's the correct way of doing this?
Reading the state the way you're doing here, is not guaranteed to give you the latest state possible.
let newRecordings = this.state.recordings
In order to use your old state to create a new one, you need to pass an updater function to setState as the first argument. Look at the examples here in React docs: https://reactjs.org/docs/react-component.html#setstate
Then you can create your new state based on your old state and set it as the new state by returning it from your updater function. This way you can be sure that your new state is what you expect it to be.
Moreover, since your state is an array, its always passed/refered to by reference and React always suggests to not mutate a state (here, we're mutating it by pushing a value to it), but instead, create a new one by cloning the previous one. So the final setState should look like this:
this.setState((prevState) => {
return {
recordings: [...prevState.recordings, response],
}
})
I have a situation where I should get a song item by id to get the path for that song, and then navigate to that song on button click.
Is there any specific hook that can be used to navigate on data arrival, useEffect will be called any time that state changes but the problem is that first needs to be dispatched the action to get the song, check if it returns any item and then navigate. Typically if it is has been published on the list, it should exist on the db, but the problem might be at the API side, so that check results.length > 0 is why that check is necessary.
useEffect(() => {
const handleClick = (myId: string) => {
dispatch(SongActions.searchSong(myId));
if (results.length > 0) {
if (Object.keys(results[0]).length > 0) {
// navigate(`/songs/${results[0].myPath}`);
}
}
}
}, [dispatch, results])
When user clicks on list item which has a song title, it should call the function handleClick(id) with id of the song as parameter, that is to get the metadata of the song, src path etc.
<Typography onClick={() => handleClick(songItem.songId)} sx={styles.songListItemText}>{songItem.Title}</Typography>
Edit
And this is how I have setup the searchSong action:
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch({
payload: { isLoading: true },
type: SearchActionType.REQUEST,
});
try {
const response = await SearchApi.searchSongAsync(obj);
if (response.length === 0) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch({
type: SearchActionType.RECEIVED_SONG,
payload: { results: response },
});
} catch (e) {
console.error("Error: ", e);
}
}
You appear to be mixing up the purpose of the useEffect hook and asynchronous event handlers like button element's onClick handlers. The useEffect hook is to meant to issue intentional side-effects in response to some dependency value updating and is tied to the React component lifecycle, while onClick handlers/etc are meant to respond to asynchronous events, i.e. a user clicking a button. They don't mix.
Assuming SongActions.searchSong is an asynchronous action, you've correctly setup Redux middleware to handle them (i.e. Thunks), and the action returns the fetched response data, then the dispatched action returns a Promise that the callback can wait for.
Example:
const navigate = useNavigate();
const dispatch = useDispatch();
const handleClick = async (myId: string) => {
const results = await dispatch(SongActions.searchSong(myId));
if (results.length > 0 && Object.keys(results[0]).length > 0) {
navigate(`/songs/${results[0].myPath}`);
}
};
...
<Typography
onClick={() => handleClick(songItem.songId)}
sx={styles.songListItemText}
>
{songItem.Title}
</Typography>
The searchSong action creator should return a resolved value for consumers to await for.
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch(startRequest());
try {
const results = await SearchApi.searchSongAsync(obj);
if (!results.length) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch(receivedSong({ results }));
return results; // <-- return resolved value here
} catch (e) {
console.error("Error: ", e);
} finally {
dispatch(completeRequest());
}
}
You can create a state such as const [isDataPresent, setIsDataPresent] = useState(false) to keep track of if the data has arrived or not. And as David has mentioned in the comments you cannot call the function inside the useEffect on handleClick. Instead what you can do is create that function outside the useEffect hook and inside the same function you fetch the data and check if the data is at all present, if present then you can set the above boolean state to true and then redirect from that function itself.
Since you are already fetching the data from the same API and different endpoint, what you can do is -
Create a new component.
Since you are mapping over the data send the data to this component by rendering it inside the map function. It'd allow the data to be passed and components to be rendered one by one.
Create a state in the new component.
Use useEffect hook to fetch the data for a single song since when you are passing the data from the previous component to this one you would also get access to the ID and store it inside the state. This would be occurring inside the newly created component.
In a React project, I have a state gameResults with a array of games, and I have a function to get the list of games based on a query :
useEffect(() => {
const timeoutId = setTimeout(() => {
if (gameQuery.length > 0) {
axios.get(`/api/games/${gameQuery}`).then((response) => {
const igdbGames: IGDBGame[] = response.data.games;
const formatedGames = formatGames(igdbGames);
setGameResults(formatedGames);
});
}
}, 300);
return () => clearTimeout(timeoutId);
}, [gameQuery]);
For each game, I don't have the cover, so I get the cover for each game :
const loadGamesImages = async () => {
for (let i = 0; i < gameResults.length; i++) {
axios
.get(`/api/cover/${gameResults[i].id}`)
.then((response) => {
const coverUrl: IGDBCover = response.data.covers[0];
const newGame = {
...gameResults[i],
cover: coverUrl.url.replace("//", "https://"),
};
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
})
.catch((error) => {
console.log("error", error);
});
await sleep(300);
}
console.log("finish");
};
useEffect(() => {
loadGamesImages();
}, [gameResults.length]);
Here is my problem : when React update the state, the old state is not there anymore. I explain : for the first cover, it's ok the new state has the first game covered. But when he make a new state for the second game, as you can see i get the gameResults state, but in this one the first game has no cover anymore.
Here is the result :
What have I done wrong ?
Each one of your looped asynchronous calls closes over the initial binding of the stateful gameResults - and gameResults starts out empty. For example, with the first Promise that resolves, these line:
const newGames = gameResults.filter(
(game: Game) => game.id !== newGame.id
);
setGameResults([...newGames, newGame]);
have the gameResults refer to the empty array, so setGameResults properly spreads the empty array plus the just-added newGame.
But then on further Promise resolutions, they also close over the initially-empty gameResults - all the async calls happened before the component re-rendered.
Use a callback instead, so that the async calls don't overwrite each other:
setGameResults((gameResults) => {
const newGames = gameResults.filter(
(game) => game.id !== newGame.id
);
return [...newGames, newGame];
});
(also note that there's no need to explicitly note the type of a parameter that TS can already infer automatically: (game: Game) can be just game)
Once this is working, I'd also suggest tweaking your code so that, when the effect hook runs again, only covers that have not been retrieved yet get requested again. This'll save you from unnecessarily making duplicate requests.
I have 3 components in react. One of them is the parent component and the remaining two are the child components (EziSchedule & EziTransaction), where each component is getting its own data by calling the API. however data to show in child component EziTransaction depends upon what record I click on table in schedule class.
when user click on record in EziSchedule, it goes back to parent component followed by EziTransaction and can print correct id in EziTransaction.
I need to refresh data in EziTransaction component which I am unable to do so. I believe I need to use state changed in order to call getEziTransactionData in EziTransaction component to refresh data but not sure how to do it.
Parent component
const EziTrackerParrent = () =>{
const [data, setData] = useState('schedule');
useEffect(() =>{
},[]);
return (
<div>
<h3>EziSchedule Table</h3>
<EziSchedule change={setData} ></EziSchedule>
<h3>EziTransaction Table</h3>
<EziTransaction data={data}></EziTransaction>
</div>
)
Child Component A - EziSchedule
Capture click event and pass it to parent
const EziSchedule = ({change}) =>{
Child Component B - EziTransaction
Get data from EziSchidule on click event via parent component. I need help here to ensure very time 'data' value changes, It call getEziTransactionData() and refresh html
const EziTransaction = ({data}) =>{
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
useEffect(() => {
getEziTransactionData(); // this method call API to get data
},[]);
const getEziTransactionData = ()=>{ // I need to call this everytime user click on record in EziSchedule???
(async () =>{
try{
const result = //API Call...
setEziTransactionData(result);
........
return(
<div>
<div>I have received "{data}" from parent</div> // this values does change every click in EziSchedule
to trigger getEziTransactionData everytime dataupdates you need to pass data as dependency at useEffect
useEffect(() => {
getEziTransactionData();
},[data]);
You can run the useEffect hook on props/state change by adding that variable in dependency array if the useEffect hook.
In your case you can add data to dependency of useEffect and useEffect will run every time the variable data changes.
If you are using data in your method to make api call, you can make that function a callback and just add that function to the dependency of useEffect, so every time data will change, your callback function will change, which will trigger the useEffect hook.
const EziTransaction = ({data}) => {
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
// wrap this function in a useCallback hook
const getEziTransactionData = useCallback(async () => {
try {
const result = await fetchApiData(data) // assuming you are using `data` in your API Call, if not add it to useEffect dependency and remove from this callback's dependency
setEziTransactionData(result);
} catch (err) {
console.log(err)
}
}, [setEziTransactionData, data]); // set dependency array of callback properly
useEffect(() => {
getEziTransactionData();
},[getEziTransactionData]); // set dependency array properly
return(
<div>
<div>I have received "{data}" from parent</div>
</div>
);
}
in addition to the answer below i would recomend to define you fetch function inside useEffect with data dependency to avoid extra memoization
useEffect(() => {
const getEziTransactionData = async () => {
try {
const result = await fetchApiData(data);
setEziTransactionData(result);
} catch (err) {
console.error(err);
}
});
getEziTransactionData();
}, [data]);
I am using React hooks and I need to store the data from my server which is an object, to a state.
This is my state:
const [sendingTime,setSendingTime] = useState({})
This is how I set it inside useEffect:
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
},[])
Now when I console.log the object state it returns an empty object. Although if I set the object to any variable and then console.log it it doesn't return an empty object. But both ways I am unable to access the properties of the object.
Edit
This is what I was doing previously:
const[time,setTime] = useState({
totalComplaintsTime: '00:00',
resolvedComplaintsTime: '00:00',
unresolvedComplaintsTime:'00:00',
assignedComplaintsTime:'00:00',
supervisorsTime:'00:00',
rejectedComplaintsTime:'00:00'
})
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setTime({
totalComplaintsTime: res.data.Complaints[0].updatedAt,
resolvedComplaintsTime: res.data.Resolved[0].updatedAt,
unresolvedComplaintsTime: res.data.Unresolved[0].updatedAt ,
assignedComplaintsTime: res.data.Assigned[0].updatedAt ,
rejectedComplaintsTime: res.data.Rejected[0].updatedAt,
supervisorsTime: res.data.Supervisors[0].updatedAt
})
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
// getTime()
getTime()
},[])
And this is how I used the states to set the dynamic values :
Last updated at {time.resolvedComplaintsTime}
This is working perfectly fine but I wanted to store the data in an object state and then access it, which would've been easier and more efficient. Also I wanted to pass this object to another component. That is why I wanted to make a state and store the data in that state.
Solved
So the main problem was accessing the data throughout the component. This is the solution:
sendingTime is being initialized but only when a render occurs. So we add a piece of code to check if that state is initialized or not.
This is where I wanted to display the data.
<div key={sendingTime.length} className={classes.stats}>
<UpdateIcon fontSize={"small"} /> Last updated at{" "}
{Object.keys(sendingTime).length > 0 &&
sendingTime.Complaints[0].updatedAt}
</div>
This way I can access the properties of the object stored in the sendingTime state very easily.
setSendingTime comes from a useState so it is asynchronous:
When you call:
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
The state sendTime has not been updated, so it display the init value which is {}
The other answers are correct, setState is asynchronous so you will only be able to get sendingTime's new value on the next re-render. And as #Enchew mentions, you probably don't want to set an object as that value most likely.
Try this instead:
const [data, setData] = useState(undefined)
const getTime = () => {
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(({ data }) => {
console.log(data)
setData(data)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
}, [])
if (!data) return <p>No data...</p>
return (
<>
<p>Complaints: {data.Complaints[0].updatedAt}<p>
<p>Resolved: {data.Resolved[0].updatedAt}<p>
<p>{/* ...etc... */}</p>
</>
)
You should see the value you're expecting to see in the console.log because you're using the locally scoped variable, not the asynchronous value, which will only be updated on the next re-render.
setSendingTime works asynchronously and your object may have not been saved in the state yet. Also pay attention to the way you save the time in your state, you are wrapping the result of the data in another object with property sendingTime, which will result in the following object: sendingTime = { sendingTime: {/*data here */} }. If you are running for sendingTime: {/*data here */} try the following:
const getTime = () => {
axios
.get('https://localhost:1999/api/getLastUpdatedTime')
.then((res) => {
console.log(res.data);
setSendingTime(res.data);
console.log('sendingTime is coimng', sendingTime);
})
.catch((err) => console.error(err));
};
Also have a look at this - how to use hooks with a callback: https://www.robinwieruch.de/react-usestate-callback