Check setState has run before executing API call in React - reactjs

I have a form with a text field and a form input which accepts multiple files. OnSubmit the files are sent to Firebase Storage, which sends back a URL for each file. These URLs are then stored in a 'photosURL' array in the form object, which is then posted to MongoDB.
The problem is, every time I post the form object data to Mongo, the photos array is empty, despite the console log showing it to be populated before I call the post-to-Mongo code. This leads me to think the post-to-Mongo code is using the form object value before it has been populated with the photo URLs.
The question is, how do I check that the photo array has been populated before I run the code to push the data to MongoDB? I'm already using a Promise.all to in theory wait for all the files to be sent and the URLs returned, but I can't work out why else the photoURLs array is empty every time data is sent to Mongo.
Here's the code:
const [form, setForm] = useState({
userId: '',
post: '',
createdAt: createdAt,
photoURLs: [],
})
const handleSubmit = (e) => {
e.preventDefault()
newPost ? postData(form) : ...
}
// SEND FILE TO FIREBASE AND GET BACK THE URL
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
return fileRef.put(file).then(() => {
return fileRef.getDownloadURL().then(function (url) {
photoArray.push(url);
setForm(prevState => ({ ...prevState, photos: photoArray }))
});
});
}
// POST FUNCTION
const postData = async (form) => {
setLoading(true)
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
Promise.all(uploadTasks).then(() => {
axios.post('/api/posts', form)
.then(response => {
...
})
.catch(error => {
...
})
})
}
Can anyone see what's going wrong, please?
EDIT: This is a consolel log of the form object, called before the axios.post code (it's showing the photosURL as populated):
createdAt: 1630072305502
photos:
0: "https://firebasestorage.googleapis.com/..."
1: "https://firebasestorage.googleapis.com/..."
post: "sample text"
userId: "1iNGV..."

I think that you are running into a timing issue.
Don't forget that React state updates are asynchronous, as described here.
I suggest to pass your URLs directly instead of going through your component's state:
async function handleUpload(file) {
const storageRef = useStorage.ref("PostImages");
const fileRef = storageRef.child(`${nanoid()}`);
await fileRef.put(file);
const url = await fileRef.getDownloadURL();
return url; // Send back the download URL
}
const postData = async (form) => {
setLoading(true);
let thisFileArray = fileInput.current.files;
const uploadTasks = [];
for (let i = 0; i < thisFileArray.length; i++) {
uploadTasks.push(handleUpload(thisFileArray[i]));
}
const photos = await Promise.all(uploadTasks); // Get all URLs here
await axios.post('/api/posts', {...form, photos}); // Send URLs to your server
setLoading(false);
}

If I understood correct, You want to upload files first and when you get your urls array populated then only you want to call postData function?
If that's the case, then you can use useEffect to detect the urls change.
useEffect(() => {
// Here you call your postData function.
postData();
}, [form.photoURLs])
What this will do is, Whenever your form.photoURLs gets populated this useEffect will run and will make the request to the server with proper data.

Related

fetch vs. axios - what is the difference in error handling?

I have an app where I'm updating the API URL via searchbox and hence displaying certain items based on their name. The name parameter is being updated via onChange event in the input and then useEffect re-renders the list of the items as it observes the URL.
If the name parameters does not match any items, the page should show that no data was found.
Now, the thing is, that the feature works when I'm using fetch API as even if there's 404 code status, the fetchedData state seems to still get updated with the object below:
fetch
and "no data found" can be displayed.
However, when I use axios, fetchedData keeps the latest displayable list of items and I'm not able to show the communicate. Here's what I see in the console:
enter image description here
const [fetchedData, setFetchedData] = useState('');
const [search, setSearch] = useState('');
const { info, results } = fetchedData;
const url = `https://rickandmortyapi.com/api/character/?name=${search}`;
// useEffect(() => {
// (async function () {
// try {
// const response = await axios.get(url);
// console.log(response.data);
// setFetchedData(response.data);
// } catch (err) {
// console.log(err);
// }
// })();
// }, [url]);
useEffect(() => {
(async function () {
const response = await fetch(url);
const data = await response.json();
console.log(data);
setFetchedData(data);
})();
}, [url]);
Does anyone have any idea what's behind it and how that mechanism can be implemented with axios? When working the axios way, I tried to set fetcheData state to null in case of error but this did not work.

Redux subscribed async State empty

I have a Login Handler which takes the response and dispatches data to redux (userdata and json web tokens). I also do have an interval to renew JWTs every X minutes.
However the subscribed state returns undefined and I´m quite unsure why. If I track the redux store there is data written successfully to it. What do I miss here?
const App = () => {
const dispatch = useDispatch()
const logedInUser = useSelector(state => state.logedInUser.logedInUser)
const tokens = useSelector(state => state.token.tokens)
const intervalRef = useRef();
const refreshToken = useCallback(async () => {
console.log(tokens.refreshToken) //prints undefined
console.log(logedInUser.id) //prints undefined
let response = await restAPI("post", hostAPI + "/refreshToken", {token: tokens.refreshToken})
dispatch(setTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken
}))
}, [])
useEffect(() => {
const interval = setInterval(() => refreshToken(), 30000)
intervalRef.current = interval;
return () => clearInterval(interval)
}, [refreshToken])
const loginHandler = async (data) => {
let response = await restAPI("post", hostAPI + "/login", {email: data.email, password: data.password}) //Simple Fetch with Post method returns response as JSON
if(response.user.id) {
console.log("RESPONSE",response) //Prints correct Response
dispatch(setLogedInUser(response.user)) //Dispatch returned Data to redux
dispatch(setTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken
}))
}
}
TokenSlice as example (redux toolkit used):
const initialState = {
tokens: []
}
export const tokenSlice = createSlice({
name: "token",
initialState,
reducers: {
setTokens: (state, action) => {
state.tokens = action.payload
console.log("Token:", state.tokens) //Prints out correct Tokens afterDispatch
},
}
})
If I build up a Component with a button in which I Refresh the Token on click everything works as expected. I´m sure that it is just a silly little thing what I´m missing here but since I´m pretty new to Redux I can´t point out what is the issue here is.
Thanks in advance!
Edit:
I´ve noticed something else. If I change some code and Save it the next "Refresh Token Interval" does print out the correct values. Means somehow "tokens" never updates from the Initial empty state - at least in app component. Like mentoined the redux state itself holds the right values.
I think your method of fetching data from API is wrong. I'm seeing that in your code snippets. You are not using any package like Axios or API request mode. Please see any tutorial on how to fetch data from API?
Try to do this work by using :
const Axios = require("axios");
Axios.defaults.withCredentials = true;

How to stop variable re initializing in context

I have a variable in my context with the base url for my api and i add parameters to that base call to add parameters, the problem that i have is that i can't concatenate more than one parameter at a time, so if for example set my checkbox for one brand the url updates to that brand filter and the function adds the brand filter parameter, but if i search something the url re-initialites itself and deletes the other parameter.
Some images for more clarity:
If i make a search:
The url console logs like this, the line 87 logs the url before i concatenate the parameter and the 91 after i concatenate the parameter:
And if i check the Nike brand that should only show the Nike Air Yeezy product and url should be "http://localhost:5000/api/v1/products?name=Yeezy&company=Nike" is only "http://localhost:5000/api/v1/products?company=Nike&"
My theory is that as i declarate the base url parameter every time i do one function the context resets and that url var re initializes, but how can i stop this? I searched at how to make a string state and concatenating that state but couldn't find anything.
Here is the part of the context where i handle the new api calls, if something seems weird/missing is because i cut the context to make it more clear
var url = "/api/v1/products?";
const [productList, setProductList] = useState([]);
const [brandFilter, setBrandFilter] = useState([]);
const [search, setSearch] = useState({
text: "",
});
const getProductList = async () => {
const response = await fetch(url);
const responseJson = await response.json();
if (responseJson) {
setProductList(responseJson.products);
}
};
useEffect(() => {
getProductList();
}, []);
useEffect(() => {
setSearchUrl();
getProductList();
}, [search]);
const setBrandUrl = () => {
console.log(url);
if (brandFilter.length > 0) {
for (var i = 0; i < brandFilter.length; i++) {
url += `company=${brandFilter[i]}&`;
}
}
console.log(url);
};
const setSearchUrl = () => {
console.log(url);
if (search.text != "") {
url += `name=${search.text}`;
}
console.log(url);
};
useEffect(() => {
setBrandUrl();
getProductList();
}, [brandFilter]);
It's because you've got the useEffect split into two; one which is reacting to the search changing, and one which is reacting to the brands changing. You need to combine those into one.
If we look at the effect for when the search changes, and we reduce the code to what's actually running, your effect is doing this:
useEffect(() => {
var url = "/api/v1/products?";
if (search.text != "") {
url += `name=${search.text}`;
}
getProductList();
}, [search]);
i.e. it ignores brandsFilter completely. You need to have both the brandsFilter and the search as a dependency for a single effect:
useEffect(() => {
let url = "/api/v1/products?";
if (search.text) {
url += `name=${search.text}`;
}
if (brandsFilter.length) {
const brands = brandsFilter.map(b => `&brand=${b}`).join('');
url += search.text ? brands : brands.substr(1);
}
// NOTE: pass the 'url' to the get as a parameter here!
getProductList(url);
}, [search, brandsFilter]);
Note: this is easier if you use a library like query-string to do the heavy lifting for you:
import qs from 'query-string';
useEffect(() => {
const url = qs.stringifyUrl({
url: "/api/v1/products?",
query: {
name: search.text || undefined,
brands: brandsFilter,
}
});
getProductList(url);
}, [search, brandsFilter]);

How to display all successful and unsuccessful request to an API in Next.js?

I have an input in which I write the names of the characters through a comma Ricky, Marty, etc.
Accordingly, on each of the heroes, I make requests in a database and show results.
How do I display a list of successful and unsuccessful requests if the hero is not found?
export const getServerSideProps: GetServerSideProps = async (context) => {
const { name } = context.query;
const nameArray = (name as string).split(',');
const allRequest = nameArray.map((el) => axios.get(`https://rickandmortyapi.com/api/character/?name=${el}`));
const charactersList = await axios.all(allRequest)
.then(axios.spread((...response) => response.map((e) => e.data.results)));
return ({
props: {
charactersList,
},
});
};
With this code, I just get the data from the database. And I need it
Ricky (data from input) --- data from database Morty (data from input --- data from database)
, etc and the list of which was not found.
You probably want to use Promise.allSettled() to wait for all promises to either resolve or reject (and avoid rejecting everything if one of them rejects).
export const getServerSideProps: GetServerSideProps = async (context) => {
const { name } = context.query;
const nameArray = Array.isArray(name) ? name : [name];
const allRequest = nameArray.map((el) =>
axios.get(`https://rickandmortyapi.com/api/character/?name=${el}`)
);
const charactersList = await Promise.allSettled(allRequest).then((res) => {
// Iterate over all results, both successful or unsuccessful
return res.map((result) => {
// Returns response data if successful, or `undefined` otherwise
// Handle this however you like
return result.value?.data.results;
});
});
//...
}
Note that you should avoid using axios.all/axios.spread as they've been deprecated.

Login status in React

I created authorization in javascript. Then if success login I redirect to React project with url parameter http://localhost:3000/?phoneNum=%2B77072050399
Then in React I get userId by using the passed url parameter => phoneNumber using axios.
I realized it in App.js. Code below:
let url = window.location.href;
let params = (new URL(url)).searchParams;
const userPhoneNum = encodeURIComponent(params.get('phoneNum'));
const [userToken, setUserToken] = useState(null);
const getUserToken = async() => {
try {
const data = await axios
.get(`https://stormy-escarpment-89406.herokuapp.com/users/getToken?phone_number=${userPhoneNum}`)
.then(response => {
setUserToken(response.data);
})
.catch(function(error) {
console.log('No such user! Error in getting token!');
});
} catch (e) {
console.log(e);
}
}
useEffect(() => {
getUserToken();
console.log(userToken);
}, userToken);
So, when I go to next page localhost:3000/places, it is requesting for userToken again with parameter null, because there is no param phoneNum.
How to make it to request only one time and save the userId after it is taken in main page. So, then only when I click LogOut button reset the variable where userID is saved.
If you want to do that without using any third party libraries you can use browser's in built storage api
So, when you receive the token, you can store that in the local storage of the browser using localstorage.setItem and later when you wan to see if the token is there or not just read from there using localStorage.getItem
const getUserToken = async() => {
try {
const data = await axios
.get(`https://stormy-escarpment-89406.herokuapp.com/users/getToken?phone_number=${userPhoneNum}`)
.then(response => {
setUserToken(response.data);
Storage.setItem('token',JSON.stringify(response.data))
})
.catch(function(error) {
console.log('No such user! Error in getting token!');
});
} catch (e) {
console.log(e);
}
}
For Logout you can simply remove the token using localStorage.removeItem
You can easily achieve this by using the react-cookie library
npm i react-cookie
Can be easily implemented in your code by
cookies.set('key', value, { path: '/' });
cookies.get('key')
After getting the userNumber form the param
const userPhoneNum = encodeURIComponent(params.get('phoneNum'));
cookies.set('userphoneNum', userPhoneNum);
View the documentation for more information
https://www.npmjs.com/package/react-cookie

Resources