React component doesn't re-render after setState - reactjs

i have state vacations, i set it after fetch within useEffect, i have button approve that will change data in vacation state and i want to re-render component after that happens within function handleApprove , so i made up virtual state componentShouldUpdate with initial value of false and passed it as a dependency for useEffect, and when function handleApprove gets triggered, i setState to the opposite of its value !componentShouldUpdate, but the component only re-render when i click 2 times, why is that happening and why it works fine when i setState componentShouldUpdate from a child component ?
function VacationsComponent() {
const [vacations, setVacations] = useState([{}]);
const [componentShouldUpdate, setComponentShouldUpdate] = useState(false);
useEffect(() => {
const getVacations = async () => {
const response = await fetch("http://localhost:8000/get-vacations");
const data = await response.json();
setVacations(data);
};
getVacations();
}, [componentShouldUpdate]);
const handleApprove = async (e, vactionId) => {
(await e.target.value) === "approve"
? fetch(`http://localhost:8000/approve-vacation/${vactionId}`, {
method: "POST",
})
: fetch(`http://localhost:8000/reject-vacation/${vactionId}`, {
method: "POST",
});
setComponentShouldUpdate(!componentShouldUpdate);
};
<button onClick={(e) => handleApprove(e, item._id)}>
APPROVE
</button>
}

This is most probably caused because useState hook operates asynchronously. Read more here.
You can update your code to use only one state like this
function VacationsComponent() {
const [vacations, setVacations] = useState([{}]);
const getVacations = async () => {
const response = await fetch("http://localhost:8000/get-vacations");
const data = await response.json();
setVacations(data);
};
useEffect(() => {
getVacations();
}, []);
const handleApprove = async (e, vactionId) => {
const slug =
e.target.value === "approve" ? "approve-vacation" : "reject-vaction";
await fetch(`http://localhost:8000/${slug}/${vactionId}`, {
method: "POST",
});
getVacations();
};
<button onClick={(e) => handleApprove(e, item._id)}>APPROVE</button>;
}

put the setComponentShouldUpdate(!componentShouldUpdate) inside a thenable like this, and remove the async/await construct.
Also what was the intended purpose for setting state, I don't see the boolean being used anywhere. Usually when setting state you want the DOM to be updated somewhere, and especially with a boolean its great for toggling elements on the screen.
const handleApprove = (e, vactionId) => {
e.target.value === "approve"
? fetch(`http://localhost:8000/approve-vacation/${vactionId}`, {
method: "POST",
}).then(()=>{
// does this go here if it is approved or when it s rejected
setComponentShouldUpdate(!componentShouldUpdate);
})
: fetch(`http://localhost:8000/reject-vacation/${vactionId}`, {
method: "POST",
}).then(()=>{ setComponentShouldUpdate(!componentShouldUpdate); });
};

Related

await useState in React

I've been fighting with this code for days now and I'm still not getting it right.
The problem:
I'm working with a form that has a dropzone. On submit handler, I need to save the images' url in an array, but it's always returning as an empty array.
Declaring images array:
const [images, setImages] = useState([]);
Here I get the images' url and try to save them in the array:
const handleSubmit = () => {
files.forEach(async(file)=> {
const bodyFormData = new FormData();
bodyFormData.append('image', file);
setLoadingUpload(true);
try {
const { data } = await Axios.post('/api/uploads', bodyFormData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${userInfo.token}`,
},
});
setImages([...images,data])
setLoadingUpload(false);
} catch (error) {
setErrorUpload(error.message);
setLoadingUpload(false);
}
})
}
Here I have the submitHandler function where I call the handleSubmit():
const submitHandler = (e) => {
e.preventDefault();
handleSubmit();
dispatch(
createCard(
name,
images,
)
);
}
I know it's because of the order it executes the code but I can't find a solution.
Thank you very much in advance!!!!
Issue
React state updates are asynchronously processed, but the state updater function itself isn't async so you can't wait for the update to happen. You can only ever access the state value from the current render cycle. This is why images is likely still your initial state, an empty array ([]).
const submitHandler = (e) => {
e.preventDefault();
handleSubmit(); // <-- enqueues state update for next render
dispatch(
createCard(
name,
images, // <-- still state from current render cycle
)
);
}
Solution
I think you should rethink how you compute the next state of images, do a single update, and then use an useEffect hook to dispatch the action with the updated state value.
const handleSubmit = async () => {
setLoadingUpload(true);
try {
const imagesData = await Promise.all(files.map(file => {
const bodyFormData = new FormData();
bodyFormData.append('image', file);
return Axios.post('/api/uploads', bodyFormData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${userInfo.token}`,
},
});
}));
setImages(images => [...images, ...imagesData]);
} catch(error) {
setErrorUpload(error.message);
} finally {
setLoadingUpload(false);
}
}
const submitHandler = (e) => {
e.preventDefault();
handleSubmit();
}
React.useEffect(() => {
images.length && name && dispatch(createCard(name, images));
}, [images, name]);
To prevent race-conditions, you could try to use the setImages with the current value as follows:
setImages(currentImages => [...currentImages, data])
This way, you will use exactly what is currently included in your state, since the images might not be the correct one in this case.
As another tip, instead of looping over your files, I would suggest you map the files, as in files.map(.... With this, you can map all file entries to a promise and at the end, merge them to one promise which contains all requests. So you can simply watch it a bit better.
Just await your map function with an await Promise.all() function. This will resolve all promises and return the filled array

how do I perform conditial query params in axios?

I am trying to build a conditional dynamic react component where makes an API call based on the user interaction, but if the user types something in the search bar. I want to add search= param the otherwise use /list endpoint without query params. I am using currently Axios , and I would like to know some approach to do the following
const FeedsList = () => {
const [feed, setFeed] = useState([]);
const [currentPageUrl, setCurrentPageUrl] = useState("http://localhost:8001/api/v1/feeds/list/")
const performSearch = () => {
//setLoading(true)
api.get(currentPageUrl).then(res => { // axios call
setLoading(false)
setFeed(res.data.results)
}).catch(function(error){
console.log(error);
});
}
const handleSearch = (e) =>{
console.log(e.target.value)
//performSearch();
}
useEffect(() => {
performSearch()
}, [currentPageUrl]);
if (loading) return "Loading..."
}
export const api = axios.create(
{baseURL : 'http://localhost:8001/api/v1/feeds/list/'}
)
user input
<input type="text" placeholder="Enter keyword" onChange={event => handleSearch(event)}/>
Store user input to state, not URL, and then construct your URL from initial value (list) and user input, if any:
const FeedsList = () => {
const [loading, setLoading] = useState(false);
const [feed, setFeed] = useState([]);
const [searchString, setSearchString] = useState("");
const performSearch = (searchString) => {
setLoading(true);
let url = "http://localhost:8001/api/v1/feeds/list/";
// you might want to escape this value, and sanitize input on the server
if (searchString) url = `${url}?search=${searchString}`;
const cancelTokenSource = axios.CancelToken.source();
return api
.get(url, { cancelToken: cancelTokenSource.token })
.then((res) => {
setLoading(false);
setFeed(res.data.results);
})
.catch(function (error) {
console.log(error);
});
return cancelTokenSource;
};
const handleSearch = (event) => {
setSearchString(event.target.value);
};
useEffect(() => {
let token = performSearch(searchString);
return () => token.cancel();
}, [searchString]);
if (loading) return "Loading...";
};
You might want to debounce or throttle requests, so you will not bombard your server with requests on each keystroke
The Axios api allows for passing a second parameter to the get method which is the config for the request being sent. That config object takes a params property that would be parsed and appended onto the url as a query string. The nice thing is if the params object is empty it will be like not passing anything at all. A fuller example here on their GitHub page.
Passing an empty params object is the same as passing no params object at all in terms of what is requested.
// both lines request url looks like this => https://jsonplaceholder.typicode.com/users
axios.get('https://jsonplaceholder.typicode.com/users')
axios.get('https://jsonplaceholder.typicode.com/users', { params: {} })
To answer your question you could conditionally just create the params based on whether there is a value from the search input, something like the following:
const performSearch = () => {
const config = search === '' ? {
params: {}
} : {
params: { search } // same as { search: search }
}
api.get(currentPageUrl, config).then(res => {
// do something with response
}).catch(function(error){
console.log(error);
});
}
An assumption in the above would be that you stored the search value in state somewhere and add it to your useEffect dependencies list, referencing it in performSearch.

ts/react - fetch in useEffect gets called multiple times

in my functional component I want to fetch data once the component mounts. But unfortunately, the request gets fired three times, until it stops. Can you tell me why?
const [rows, setRows] = useState<any[]>([]);
const [tableReady, setTableReady] = useState<boolean>(false);
const [data, setData] = useState<any[]>([]);
const getData = async () => {
const user = await Amplify.Auth.currentAuthenticatedUser();
const token = user.signInUserSession.idToken.jwtToken;
const apiurl = 'xxx';
fetch(apiurl, {
method: 'GET',
headers: {
'Authorization': token
}
})
.then(res => res.json())
.then((result) => {
setData(result);
})
.catch(console.log)
}
useEffect(() => {
if (!tableReady) {
getData();
if (data.length > 0) {
data.forEach((element, i) => {
const convertedId: number = +element.id;
setRows(rows => [...rows, (createData(convertedId, element.user))]);
});
setTableReady(true);
}
}
}, []);
return (
<div className={classes.root}>
<MUIDataTable
title={""}
data={rows}
columns={columns}
/>
</div>
);
I updated my question due to the comment.
The useEffect is missing a dependency array, so its callback is invoked every time the component renders.
Solution
Add a dependency array.
useEffect(() => {
if (!tableReady) {
getData();
if (data.length > 0) {
data.forEach((element, i) => {
const convertedId: number = +element.id;
rows.push(convertedId);
});
setTableReady(true);
}
}
}, []); // <-- dependency array
An empty dependency array will run the effect once when the component mounts. If you want it to ran when any specific value(s) update then add these to the dependency array.
See Conditionally firing an effect
Edit
It doesn't appear there is any need to store a data state since it's used to populate the rows state. Since React state updates are asynchronously processed, and useEffect callbacks are 100% synchronous, when you call getData and don't wait for the data to populate, then the rest of the effect callback is using the initially empty data array.
I suggest returning the fetch request from getData and just process the response data directly into your rows state.
const getData = async () => {
const user = await Amplify.Auth.currentAuthenticatedUser();
const token = user.signInUserSession.idToken.jwtToken;
const apiurl = 'xxx';
return fetch(apiurl, {
method: 'GET',
headers: {
'Authorization': token
}
});
}
useEffect(() => {
if (!tableReady) {
getData()
.then(res => res.json())
.then(data => {
if (data.length) {
setRows(data.map(element => createData(+element.id, element.user)))
}
})
.catch(console.error)
.finally(() => setTableReady(true));
}
}, []);

Why do I need to put the function inside a setState method for it to work?

When a socket emits an event from the server side the App reloads for some reason and posts is emptied out. But when I define the function inside the setPosts it works perfectly. Why is this?
const App = () => {
let [user, setUser] = useState(null)
let [posts, setPosts] = useState({})
console.log('app')
useEffect(() => {
console.log('use effect')
socket.on('post', (post) => {
// THIS DOES NOT WORK:
// let newPosts = { ...posts }
// newPosts[post._id] = post
// setPosts(newPosts)
//THIS WORKS
setPosts((posts) => {
let newPosts = { ...posts }
newPosts[post._id] = post
return newPosts
})
})
async function getUser() {
let user = await actions.getUser()
if (user) {
setUser(user?.data)
}
}
getUser()
actions
.getAllPosts()
.then((res) => {
console.log('WE GOT ALL POSTSTFOM API', res.data)
const postsById = {}
for (let post of res.data) {
postsById[post._id] = post
}
console.log('wired')
setPosts(postsById)
//filterPosts(res.data)
})
.catch((err) => console.error(err))
return () => {
socket.off('post')
}
}, [])
This is how enclosures work in javascript. When you use a non-functional state update you are referencing the posts state value ({}) from the render cycle the callback was instantiated in, i.e. the initial render when the effect callback ran when mounted (note the empty dependency array). It's a stale enclosure of the posts state value.
When using a functional state update you are accessing and updating from the previous state, not the state from the previous render cycle (or enclosure).

handling event and interchangeable state in hooks

I have this small web app which I wrote when I was studying react, of course its in Class based components
As I learnt more I've decided to move over to hooks but I can't get my mind around it, imo I think class based components was more straightforward
So in Class based components I had my configiration as follows
State:
this.state = { countryData: {updated:0,cases:0, todayCases:0, deaths:0, todayDeaths:0, recovered:0, active:0}}
Setting initial state:
async getData(){
const resApi = await Axios.get('https://corona.lmao.ninja/v2/all')
this.setState({ countryData : resApi.data })
}
componentDidMount(){ this.getData() }
And then I have a dropdown menu of options which on change changes the country data
async getCountryData(event){
if (!event) return this.getData()
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`)
this.setState({ countryData : res.data })
}
So now I've tried to make the same thing in hooks I've started with
const [countryData, setcountryData] = useState({updated:0,cases:0, todayCases:0, deaths:0, todayDeaths:0, recovered:0, active:0})
Then for the previous 2 methods
const useChoosenCountry = (event) => {
useEffect(() => {
async function getdata(event){
if (!event) {
const resApi = await Axios.get('https://corona.lmao.ninja/v2/all')
setcountryData(resApi.data)
}
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`);
setcountryData(res.data);
}
console.log(event)
getdata()
},[event])
}
But it looks like I'm really far it's not console logging the event on change
componentDidMount can be replaced with useEffect(function, []):
useEffect(() => {
getData();
}, [])
const getData = async () => {
const res = await Axios.get('https://corona.lmao.ninja/v2/all')
setcountryData(res.data)
}
const getCountryData = async (event) => {
if (!event) return this.getData()
const res = await Axios.get(`https://corona.lmao.ninja/v2/countries/${event.value}`)
setcountryData(res.data)
}

Resources