use callback with useState hook in reactjs - reactjs

i'm trying to update the user info on my database
on the handleChange function every time there is a new change but the problem that im facing is that i have to to wait for the setdata till it finish then updateUserInfo how i can solve that
const [mydata, setData] = useState({
user_gender: "",
user_relationship: "",
user_birth_day: "",
user_birth_month: "",
user_gender_interest: "",
user_birth_year: "",
user_interests: {
dancing: false,
family: false,
art: false,
photography: false,
friends: false,
travel: false
}
});
const handleChange = event => {
setData({
...mydata,
[event.target.name]: event.target.value
});
async function update() {
await updateUserInfo(mydata[event.target.name], stableDispatch);
}
update();
};

Call updateUserInfo() as a callback.
You can pass a function as a 2nd parameter to setState() which will automatically be called when the state is set.
useEffect(() => {
updateUserInfo(mydata[event.target.name], stableDispatch));
}, [mydata]);

The solution here is to copy the state in a variable which you can use to update state and the userInfo
const handleChange = event => {
const data = {
...mydata,
[event.target.name]: event.target.value
}
setData(data);
async function update() {
await updateUserInfo(data[event.target.name], stableDispatch);
}
update();
};

Related

Add object to array inside an object(React useState)

I have a following state declared
const [state, setState] = useState({
title: "",
subtitle: "",
products: [],
state.products is an array of objects e.g.
products = [{a: "", b: "", c: ""}]
On form submit I want to take the input values and append them to the products array as an object.
My submit handler doesn't do what I want. Where have I gone wrong?
function handleSubmit(e) {
const newProduct= {"a": e.target[0].value, "b": e.target[2].value, "c": e.target[1].value}
setState({...state, products: [...state.products, newProduct]})
}
You could do something like this
function handleSubmit(e) {
const newProduct = {
a: e.target[0].value,
b: e.target[2].value,
c: e.target[1].value
};
setState(prevState => ({
...prevState,
products: [...prevState.products, newProduct]
}));
}
If you console.log outside of the state setter, you will not get the latest copy of the state, i.e.,
const Component = () => {
const [state, setState] = useState(...);
function handler(e) {
...
setState(...);
console.log(state.products); // This will print the previous version of the state.
}
}
This is the expected behavior as state setters are asynchronous.
So instead I log like this,
function handler(e) {
const newProduct = ...;
...
setState(prevState => {
const newState = {...prevState, products: [...prevState.products, newProduct]}; // update state as you did before.
console.log(newState);
return newState;
});
}
You can also use
useEffect(() => {
console.log(state)
}, [state, state.products]);

Call Multiple API endpoint in useEffect React

I am having issues with calling useEffect for multiple API endpoints. Only one or the other are loaded.
My code:
const [values, setValues] = useState({
total: [],
outstanding: []
});
useEffect(() => {
DataService.dashboard_total_Amount().then((response)=>{ setValues({...values, total: response})})
ChartService.Outstanding().then((response)=>{setValues({...values, outstanding: response})})
}, [])
hope someone can point out my issue. Thanks
A better way doing the same thing by doing this you only updating state only once hence it also re-render the component once and this look more cleaner and try to use try-catch
React.useEffect(() => {
const fetchDetails = async () => {
try {
const resDataService = await DataService.dashboard_total_Amount();
const resChartService = await ChartService.Outstanding();
setValues({ outstanding: resChartService, total: resDataService });
} catch (error) {
console.log(erro);
}
};
fetchDetails();
}, []);
The function inside the useEffect acts as a closure for the values variable. To ensure you update the most current value, you can pass a function to setValues:
const [values, setValues] = useState({
total: [],
outstanding: []
});
useEffect(() => {
DataService.dashboard_total_Amount().then((response)=>{ setValues((values) => {...values, total: response})})
ChartService.Outstanding().then((response)=>{setValues((values) => {...values, outstanding: response})})
}, [])

Updating Multiple React State within the same event handler

These are my states using hooks:
const [adminProfile, setProfile] = useState({
locations: [],
});
const [location, setLocation] = useState({
locationName: "",
location: {},
locationPhone: "",
locationEmail: "",
staff: [],
multipleKitchens: false,
kitchens: [],
});
const [locationList, setLocationList] = useState([]);
const [locationAddress, setAddress] = useState({
streetAddress: "",
streetAddress2: "",
city: "",
state: "",
zip: "",
country: "USA"
});
I have a bunch of fields with onChange handlers and an onClick handler that needs to update 3 states in order. First, LocationAddress has to become the state of the location property within the location state. Second, the location state has to be updated with a unique ID, and then that unique ID is inserted into the array in the locationList state. Finally, the entire array from locationList state is added to the locations property of adminProfile state. These are all in one component.
const handleClickLocation = () => {
setLocation(prevValue => ({
...prevValue,
locationID: uuidv4(),
location: locationAddress
}));
setLocationList(prevValue => [...prevValue, location.locationID]);
setProfile(prevValue => ({
...prevValue,
locations: locationList
}))
The first time the click handler is triggered, it sets only the first state in the handler and sends "undefined" into the second state. When the click handler is clicked a second time, it then behaves normally. I want all the states to update simultaneously. I've tried forceUpdate(), but couldn't figure out the syntax. I've tried using ReactDOM.unstable_batchedUpdates but it still behaved the same.
How can I get this to work? I want to keep this within one component. Is that possible?
Here is the entire code updated with the useEffect hook:
import React, {useState, useEffect} from "react";
import axios from "axios";
const { v4: uuidv4 } = require('uuid');
const CompanyProfileInfo = (props) => {
const todayDate = () => {
let today = new Date();
let day = today.getDate();
let month = today.getMonth() + 1;
let year = today.getFullYear();
if (day < 10) day = '0' + day;
if(month < 10) month = '0' + month;
return (month + "/" + day + "/" + year);
}
const [adminProfile, setProfile] = useState({
companyID: props.companyInfo.companyID,
firstName: "",
lastName: "",
phonePrimary: "",
phoneSecondary: "",
emailSecondary: "",
streetAddress: "",
streetAddress2: "",
city: "",
state: "",
zip: "",
country: "USA",
multipleLocations: false,
locations: [],
membershipLevel: "Basic",
joinedDate: todayDate(),
});
const [location, setLocation] = useState({
locationName: "",
locationPhone: "",
locationEmail: "",
staff: [],
multipleKitchens: false,
kitchens: [],
});
const [locationAddress, setAddress] = useState({
streetAddress: "",
streetAddress2: "",
city: "",
state: "",
zip: "",
country: "USA"
});
const [locationList, setLocationList] = useState([]);
useEffect(() => {
setLocationList(prevValue => [...prevValue, location.locationID]);
}, [location.locationID]);
useEffect(() => {
if (locationList[0] === undefined) {
{locationList.shift()}
}
setProfile(prevValue => ({
...prevValue,
locations: locationList
})
)
}, [locationList])
const handleChange = (event) => {
const {name, value} = event.target;
setProfile(prevValue => ({
...prevValue,
[name]: value
}))
}
const handleChangeLocations = (event) => {
const {name, value} = event.target;
setLocation(prevValue => ({
...prevValue,
[name]: value
}));
};
const handleChangeLocations1 = (event) => {
const {name, value} = event.target;
setAddress(prevValue => ({
...prevValue,
[name]: value
}));
};
const handleClickLocation = () => {
setLocation(prevValue => ({
...prevValue,
locationID: uuidv4(),
location: locationAddress,
}));
};
const handleClick = () => {
axios.post('http://localhost:3001/profileinfo', adminProfile)
.then(res => {
props.supportFunctions.setUpLocations(res);
})
.catch(function (error) {
console.log(error);
})
}
return (
<div>
</div>
)
}
export default CompanyProfileInfo;
setState is asynchronous, it means that when it is called, its state won't update at the same time, it takes some time to perform its action.
You can make use of useEffect to do that.
useEffect will perform an action only when the specified state (inside brackets) changes
useEffect(() => {
setLocation({
...location,
location: locationAddress,
locationID: uuidv4()
})
}, [locationAddress]) //When locationAddress changes, setLocation
useEffect(() => {
setLocationList([
...locationList,
location.locationID
])
}, [location]) //When location changes, insert Id
Ps: You can have multiple useEffects in your code.
Updating of the state is asynchronous behavior, because of that you are getting locationID undefined for setLocationList.
Inside class component, we can use a callback to setState call like this -
this.setState({ data: newData }, () => { console.log("This will get printed after setState") })
But in your case, you are using function component so you have to use useEffect react hook to listen for changes in your data and then update other data in the state.
Take a look at this question - How to use `setState` callback on react hooks

Missing or unused dependency array in useEffect

When implementing the hook code from here
https://blog.logrocket.com/patterns-for-data-fetching-in-react-981ced7e5c56/
I get the following warning, what does this actually mean?
./src/components/Users.tsx
Line 20:6: React Hook useEffect has a missing dependency: 'data.users'. Either include it or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setData' needs the current value of 'data.users' react-hooks/exhaustive-deps
code:
import React, { useEffect, useState } from "react";
import axios from "axios";
const USER_SERVICE_URL = "https://jsonplaceholder.typicode.com/users";
export function List() {
const [data, setData] = useState({ users: [], isFetching: false });
useEffect(() => {
const fetchUsers = async () => {
try {
setData({ users: data.users, isFetching: true });
const response = await axios.get(USER_SERVICE_URL);
setData({ users: response.data, isFetching: false });
} catch (e) {
console.log(e);
setData({ users: data.users, isFetching: false });
}
};
fetchUsers();
}, []);
console.log(data)
It means that since you use data.users in the code inside the useEffect block, and you've define an empty dependencies array, data.users might be stale.
To solve that problem use the setState updater function. Which allows you to use the previous state, while creating the new one:
setData(data => ({ users: data.users, isFetching: true }));
Or
setData(data => ({ users: data.users, isFetching: false }));
Another options is to create a simple reducer, that will work like setState(), but will override changed items, and not the entire state:
const reducer = (state, payload) => ({ ...state, ...payload });
export function List() {
const [data, setData] = useReducer(reducer, { users: [], isFetching: false });
useEffect(() => {
const fetchUsers = async () => {
try {
setData({ isFetching: true });
const response = await axios.get(USER_SERVICE_URL);
setData({ users: response.data, isFetching: false });
} catch (e) {
console.log(e);
setData({ isFetching: false });
}
};
fetchUsers();
}, []);
console.log(data)

How can I access current redux state from useEffect?

I have a list of objects ("Albums" in my case) fetched from the database. I need to edit these objects.
In the editing component in the useEffect hook I fire up the action for getting the needed album using it's ID. This action works. However in the same useEffect I am trying to fetch the changed by before fired action redux state. And now I face the problem - all I am fetching is the previos state.
How can I implement in the useEffect fetching of current redux state?
I've seen similar questions here, however none of the answers were helpfull for my use case.
I am using redux-thunk.
Editing component. The problem appears in setFormData - it's fetching previous state from the reducer, not the current one. It seems that it fires before the state gets changed by the getAlbumById:
//imports
const EditAlbum = ({
album: { album, loading},
createAlbum,
getAlbumById,
history,
match
}) => {
const [formData, setFormData] = useState({
albumID: null,
albumName: ''
});
useEffect(() => {
getAlbumById(match.params.id);
setFormData({
albumID: loading || !album.albumID ? '' : album.albumID,
albumName: loading || !album.albumName ? '' : album.albumName
});
}, [getAlbumById, loading]);
const { albumName, albumID } = formData;
const onChange = e =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = e => {
e.preventDefault();
createAlbum(formData, history, true);
};
return ( //code );
};
EditAlbum.propTypes = {
createAlbum: PropTypes.func.isRequired,
getAlbumById: PropTypes.func.isRequired,
album: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
album: state.album
});
export default connect(
mapStateToProps,
{ createAlbum, getAlbumById }
)(withRouter(EditAlbum));
Action:
export const getAlbumById = albumID => async dispatch => {
try {
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
reducer
const initialState = {
album: null,
albums: [],
loading: true,
error: {}
};
const album = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_ALBUM:
return {
...state,
album: payload,
loading: false
};
case ALBUMS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
Will be grateful for any help/ideas
You should split up your effects in 2, one to load album when album id changes from route:
const [formData, setFormData] = useState({
albumID: match.params.id,
albumName: '',
});
const { albumName, albumID } = formData;
// Only get album by id when id changed
useEffect(() => {
getAlbumById(albumID);
}, [albumID, getAlbumById]);
And one when data has arrived to set the formData state:
// Custom hook to check if component is mounted
// This needs to be imported in your component
// https://github.com/jmlweb/isMounted
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
// In your component check if it's mounted
// ...because you cannot set state on unmounted component
const isMounted = useIsMounted();
useEffect(() => {
// Only if loading is false and still mounted
if (loading === false && isMounted.current) {
const { albumID, albumName } = album;
setFormData({
albumID,
albumName,
});
}
}, [album, isMounted, loading]);
Your action should set loading to true when it starts getting an album:
export const getAlbumById = albumID => async dispatch => {
try {
// Here you should dispatch an action that would
// set loading to true
// dispatch({type:'LOAD_ALBUM'})
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
Update detecting why useEffect is called when it should not:
Could you update the question with the output of this?
//only get album by id when id changed
useEffect(() => {
console.log('In the get data effect');
getAlbumById(albumID);
return () => {
console.log('Clean up get data effect');
if (albumID !== pref.current.albumID) {
console.log(
'XXXX album ID changed:',
pref.current.albumID,
albumID
);
}
if (getAlbumById !== pref.current.getAlbumById) {
console.log(
'XXX getAlbumById changed',
pref.current.getAlbumById,
getAlbumById
);
}
};
}, [albumID, getAlbumById]);

Resources