await useState in React - reactjs

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

Related

Asynchronous function makes request without data when page reloads

So the problem here is I have this asynchronous function that makes request with string variable, but when page reloads it makes same request without this string despite the fact that variable itself is not empty. As a result I am receiving an error 'Bad Request' because text was not provided. Would someone be so kindly to explain me how thing works here so i could fix it so that those requests after page reloading were sent with data ?!
const { text, setText } = useContext(TextContext);
const [isLoading, setLoading] = useState(true);
const [entitiesData, setEntitiesData] = useState([]);
const call_razor = async (text_encoded) => {
try {
console.log(text_encoded) //here it shows data even when described error occurs after
const response = await axios.post('https://api.textrazor.com/',
"extractors=entities&text="+text_encoded,
{
headers: {
'x-textrazor-key': API_KEY,
'Content-Type': 'application/x-www-form-urlencoded',
}
});
setEntitiesData(response.data.response.entities)
setLoading(false)
} catch (err) {
console.log(err)
console.log(err.response.data.error)
console.log(err.response)
setLoading(false)
}
}
const dataFetch = async () => {
let textEncoded = encodeURIComponent(text)
await call_razor(textEncoded).then(() => splitIntoSentences())
}
useEffect(() => {
if (text) {
localStorage.setItem('text', text)
} else {
setText(localStorage.getItem('text'))
}
dataFetch();
}, [isLoading]);
The problem you're encountering is likely due to the fact that the useEffect hook is running before the text value is set from the TextContext context object.
One way to fix this issue is to move the useEffect hook to the parent component that is providing the TextContext context object, and pass the text value as a prop to the child component that is making the API request. This way, the text value will be available to the child component before the useEffect hook is run.
Another way would be to add a check for the text variable being empty or not in dataFetch() function, If it's empty, you can set isLoading to false so that it doesn't trigger the useEffect callback function.
const dataFetch = async () => {
if(text){
let textEncoded = encodeURIComponent(text)
await call_razor(textEncoded).then(() => splitIntoSentences())
}else {
setLoading(false)
}
}
You can also move the dataFetch() function call inside the useEffect callback after the text value is set.
useEffect(() => {
if (text) {
localStorage.setItem('text', text)
dataFetch();
} else {
setText(localStorage.getItem('text'))
}
}, [isLoading]);

React component doesn't re-render after setState

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); });
};

why is my state not updated in useEffect?

const user = useSelector(state => state.user)
const [gioHangChiTiet, setGioHangChiTiet] = useState([])
const [gioHangSanPham, setGioHangSanPham] = useState([])
useEffect(() => {
const dataGioHang = async () => {
try {
const res = await axios.get(`${apiUrl}api/giohangs`, {
headers: {
token: `Bearer ${user.user?.accessToken}`
}
})
console.log(res.data.data.sach)
setGioHangChiTiet(res.data.data)
console.log(gioHangChiTiet "it is empty")
} catch (error) {
console.log(error)
}
}
if (user.user) {
dataGioHang()
// console.log(gioHangChiTiet)
}
}, [user])
That is my code. I trying to save gioHangChiTiet with new data but it's always is an empty array. I try console.log this and I think it will work but it's not. But if I change any thing in this code, gioHangChiTiet will update new data and console.log this. Can anyone help me and explain why? Thank you so much. I spent a lot of time figuring out how to solve it :(( . UPDATED : I fixed it. Thanks a lots ( console.log not run because it in useEffect , if i console after useEffect, i will have true value)
const user = useSelector(state => state.user)
const [gioHangChiTiet, setGioHangChiTiet] = useState([])
const [gioHangSanPham, setGioHangSanPham] = useState([])
useEffect(() => {
const dataGioHang = async () => {
try {
const res = await axios.get(`${apiUrl}api/giohangs`, {
headers: {
token: `Bearer ${user.user?.accessToken}`
}
})
console.log(res.data.data.sach)
// setGioHangChiTiet(res.data.data.sach)
setGioHangChiTiet(res.data.data)
console.log(gioHangChiTiet)
} catch (error) {
console.log(error)
}
}
if (user.user) {
dataGioHang()
// console.log(gioHangChiTiet)
}
}, [user])
Add user to your dependency array. Otherwise the useEffect wont be able to check your if statement. If you're using CRA you should get a warning in your terminal.
useEffect takes two arguments first one is callback function and second one is dependency array.
useEffect(() => {
// this is callback function
},[ /* this is dependency array */ ])
If you want to trigger the callback function every time a state changes you need to pass that state in dependency array.
useEffect(() => {
console.log(someState)
},[someState])
In above code someState will get logged each time it's value changes.
If your dependency array is empty you useEffect callback function will trigger ONLY ONCE.
In your case if you want trigger callback function on change of user state or any other state simply pass it in dependency array.
Can you give this a try:
const user = useSelector(state => state.user)
const [gioHangChiTiet, setGioHangChiTiet] = useState([])
const [gioHangSanPham, setGioHangSanPham] = useState([])
useEffect(() => {
const dataGioHang = new Promise((resolve,reject) => {
( async() => {
try {
const res = await axios.get(`${apiUrl}api/giohangs`, {
headers: {
token: `Bearer ${user.user?.accessToken}`
}
})
console.log(res.data.data.sach)
setGioHangChiTiet(res.data.data.sach)
setGioHangChiTiet(res.data.data)
console.log(gioHangChiTiet)
resolve();
} catch (error) {
console.log(error)
reject()
}})();
})
if (user.user) {
dataGioHang().then(()={ console.log(gioHangChiTiet);
})
.catch(() => console.log("Error executing dataGioHang"))
}
}, [user])

onSuccess callback in Plaid Link not updating

I've built a PlaidLink component using react-plaid-link as below. There's no issues when building with the standard way - passing in only public_token and account_id to the request body.
However, when I attempt to pass in stripeUid to the request body, only an empty string (the initial value of the stripeUid state) is passed. This is despite the value of stripeUid being updated and passed in correctly from the parent via props. For some reason stripeUid does not update within the useCallback hook even though the value is in the dependency array.
Any idea why the value is not updating?
function PlaidLink(props) {
const [token, setToken] = useState("");
const { achPayments, stripeUid } = props;
async function createLinkToken() {
const fetchConfig = {
method: "POST",
};
const response = await fetch(
API_URL + "/plaid/create-link-token",
fetchConfig
);
const jsonResponse = await response.json();
const { link_token } = jsonResponse;
setToken(link_token);
}
const onSuccess = useCallback(
(publicToken, metadata) => {
const { account_id } = metadata;
// Exchange a public token for an access one.
async function exchangeTokens() {
const fetchConfig = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
public_token: publicToken,
account_id,
stripeUid,
}),
};
const response = await fetch(
API_URL + "/plaid/exchange-tokens",
fetchConfig
);
const jsonResponse = await response.json();
console.log("Exchange token response:", jsonResponse);
}
exchangeTokens();
}, [stripeUid]
);
const { open, ready } = usePlaidLink({
token,
onSuccess,
});
// get link_token from your server when component mounts
useEffect(() => {
createLinkToken();
}, []);
useEffect(() => {
if (achPayments && ready) {
open();
}
}, [achPayments, ready, open]);
return <div></div>;
}
export default PlaidLink;
I am not familiar with Stripe API but from reading a code I see a possible issue with the code.
Following the chain of events, there is one usePlaidLink and two useEffects. When the component mounts, it createLinkToken in one of the effects and open in the other (assuming it is ready).
However, when stripeUid changes, it doesn't re-fire the effects. So, that's a hint for me.
Next, checking the source of usePlaidLink here: https://github.com/plaid/react-plaid-link/blob/master/src/usePlaidLink.ts gives me an idea: it doesn't do anything when options.onSuccess changes, only when options.token changes. This is their dependency array:
[loading, error, options.token, products]
So it looks like your code is correct as far as effects in react go, but it doesnt't work together because changing the onSuccess doesn't do anything.
How to solve:
make a pull request into the open source library to fix the issue there
inline that library into your code and fix it for yourself
use "keyed components" to unmount and mount the component again when the uid changes instead of updating to work around the issue
some other solution

How to stop useEffect from making so many requests? Empty Dependencies don't work

I have a component that updates a piece of state but I'm having issues with it
I have the state declared
const [data, setData] = useState([]);
Then in my useEffect I am
useEffect(() => {
const fetchData = async () => {
await axios
.get(
API_URL,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY
},
params:{
"titleId": id
}
}
)
.then((response) => {
setData(response.data.Item);
})
.catch((err) => {
console.error("API call error:", err.message);
});
}
fetchData();
}, [data, id])
If I declare "data" in my dependencies, I get an endless loop of requests which is obviously no good. But if I leave 'data' out from the dependencies it shows nothing, though I am successfully retrieving it in my network's tab and even when I {JSON.styringify(data)} in a div tag aI get the json content too. So the info is in the DOM, but it's not updating the components
How can I do this so I can make an initial request to load the data and not thousands of them?
I've tried the following:
a setTimeout on the callback function
the isCancelled way with a return (() => { callbackFunction.cancel(); })
And there is an Abort way of doing this too but I can't figure it out. Every example I've seen is for class components
Sorry for the vague code. I can't replicate this without lots of coding and an API. Thanks in advance
You want to set the state and then check if is different. I use a custom hook for this which uses the useRef hook:
export function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const prevData = usePrevious(data);
I don't know what your data looks like, but build a conditional from it. Inside of your useEffect you'll need something like:
if (data !== prevData) fetchData()
or
if (data.id !== prevData.id) fetchData()
You'll then add prevData to you dependencies:
[data, prevData, id]
So useEffects works with dependency.
With dependency - on changing dependency value useEffect will trigger
useEffect(() => {
// code
}, [dependency])
With empty brackets - will trigger on initial of component
useEffect(() => {
// code
}, [])
Without dependency and Brackets - will trigger on every state change
useEffect(() => {
// code
})
Do something like this, if that can help. I also used async/await so you can check that.
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(API_URL, {
headers: {
'Content-Type': 'application/json',
'X-API-KEY': API_KEY,
},
params: {
titleId: id,
},
});
setData(response.data.Item);
} catch (err) {
console.error('API call error:', err.message);
}
};
fetchData();
}, [id]);
if (!data.length) return null;
return <p>Yes, I have data</p>;
};
obviously you will get an infinit loop !
you are updating the data inside your useEffect which means each time the data changes, triggers useEffect again and so on !
what you should do is change your dependencies depending on your case for example :
const [data, setData] = useState([])
const [fetchAgain, setFetchAgain] = useState(false)
useEffect(()=> {
fetchData();
}, [])
useEffect(() => {
if(fetchAgain) {
setFetchAgain(false)
fetchData();
}
}, [fetchAgain])
now each time you want to fetch data again you need to update the fetchAgain to true

Resources