onSuccess callback in Plaid Link not updating - reactjs

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

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

Issue with MERN stack when data is coming back from Node

I am using the Fetch API to post data to my Node server. There is behaviour that I am seeing which I am unable to understand and need some help in resolving. When the user clicks a button a modal opens up with a form with a few fields. On hitting submit, the data is passed to a node server. First it creates a new record for Mongo and saves the data in the collection. After saving I want to fetch some data from a few other collections and then send the information back to my react app.
Here is what I am seeing.
If i only save the data and don't do the fetching part and send back a message like this
res.status(201).json({"message":"data received"})
Then after the modal closes in my console I see the message "Data Received".
However if I do the fetch part, which basically means a few more milli seconds of delay, then once the modal closes, this message flashes in the react console.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
DOMException: The user aborted a request.
Here is my submit handler in react
const submitHandler = async(event) => {
event.preventDefault()
try {
const responseData = await sendRequest('http://localhost:5000/challenge/createChallenge', 'POST',
JSON.stringify({
location: selectedOption,
format: selectedOption1,
date: selectedDate.toString(),
opponent: props.initialValues,
month: cMonth,
year: cYear,
player: auth.profile.MemberName
}),
{
'Content-Type': 'application/json',
Authorization: 'Bearer ' + auth.token
})
console.log(responseData)
} catch (err){
console.log(err)
}
}
And here is my http-hook, which is a custom hook I am using
import { useState, useCallback, useRef, useEffect } from 'react'
export const useHttpClient = () => {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(false)
const activeHttpRequests = useRef([])
const sendRequest = useCallback(async (url, method = 'GET', body = null, headers = {}) => {
setIsLoading(true)
const httpAbortCtrl = new AbortController()
activeHttpRequests.current.push(httpAbortCtrl)
try {
const response = await fetch(url, {
method,
body,
headers,
signal: httpAbortCtrl.signal
})
const responseData = await response.json()
activeHttpRequests.current = activeHttpRequests.current.filter(reqCtrl => reqCtrl !== httpAbortCtrl)
if(!response.ok){
throw new Error(responseData.message)
}
setIsLoading(false)
return responseData
} catch (err){
setError(err.message)
setIsLoading(false)
throw err
}
}, [])
const clearError = () => {
setError(null)
}
useEffect(() => {
return () => {
activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort())
}
}, [])
return { isLoading, error, sendRequest, clearError}
}
Can some one please help. I am not sure how to go about resolving this. I checked the reasons for this error, while some of the content on the internet gave me some clue. But I am not clear enough to resolve this.

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

Reactjs Cannot Update During an Existing State Transition

I'm creating a global function that checks whether the jwt token is expired or not.
I call this function if I'm fetching data from the api to confirm the user but I'm getting the error that I cannot update during an existing state transition and I don't have a clue what it means.
I also notice the the if(Date.now() >= expiredTime) was the one whose causing the problem
const AuthConfig = () => {
const history = useHistory();
let token = JSON.parse(localStorage.getItem("user"))["token"];
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
localStorage.removeItem("user");
history.push("/login");
} else {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
};
I'm not sure if its correct but I call the function like this, since if jwt token is expired it redirect to the login page.
const config = AuthConfig()
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
I updated this peace of code and I could login to the application but when the jwt expires and it redirect to login using history.push I till get the same error. I tried using Redirect but its a little slow and I could still navigate in privateroutes before redirecting me to login
// old
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime)
// change
if (exp < Date.now() / 1000)
i would start from the beginning telling you that if this is a project that is going to production you always must put the auth token check in the backend especially if we talk about jwt authentication.
Otherwise if you have the strict necessity to put it in the React component i would suggest you to handle this with Promises doing something like this:
const config = Promise.all(AuthConfig()).then(()=> productData());
I would even consider to change the productData function to check if the data variable is not null before saving the state that is the reason why the compiler is giving you that error.
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
data && setProduct(data);
};
Finally consider putting this in the backend. Open another question if you need help on the backend too, i'll be glad to help you.
Have a nice day!
I'm still not sure how your code is used within a component context.
Currently your API and setProduct are called regardless whether AuthConfig() returns any value. During this time, you are also calling history.push(), which may be the reason why you encountered the error.
I can recommend you to check config for value before you try to call the API.
const config = AuthConfig()
if (config) {
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
}
I'm assuming that AuthConfig is a hook, since it contains a hook. And that it's consumed in a React component.
Raise the responsibility of redirecting to the consumer and try to express your logic as effects of their dependencies.
const useAuthConfig = ({ onExpire }) => {
let token = JSON.parse(localStorage.getItem("user"))["token"];
const [isExpired, setIsExpired] = useState(!token);
// Only callback once, when the expired flag turns on
useEffect(() => {
if (isExpired) onExpire();
}, [isExpired]);
// Check the token every render
// This doesn't really make sense cause the expired state
// will only update when the parent happens to update (which
// is arbitrary) but w/e
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
setIsExpired(true);
return null;
}
}
// Don't make a new reference to this object every time
const header = useMemo(() => !isExpired
? ({
headers: {
Authorization: `Bearer ${token}`,
},
})
: null, [isExpired, token]);
return header;
};
const Parent = () => {
const history = useHistory();
// Let the caller decide what to do on expiry,
// and let useAuthConfig just worry about the auth config
const config = useAuthConfig({
onExpire: () => {
localStorage.removeItem("user");
history.push("/login");
}
});
const productData = async (config) => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
useEffect(() => {
if (config) {
productData(config);
}
}, [config]);
};

Error: Invalid hook call. when calling hook after await - how do you await for data from one hook to instantiate into another?

Im trying to inject a header into my fetcher before using swr to fetch the data. I need to await for a custom hook to respond with the data before I can inject it into the custom fetcher.
If I use a promise.then I know i'm getting the relevant data, and if I manually inject it using a string it works and i see the header. Its just doing it async
How should I go about doing this?
Code:
export const useApi = async (gql: string) => {
const { acquireToken } = useAuth()
await acquireToken().then(res => { graphQLClient.setHeader('authorization', `Bearer ${res.accessToken}`) })
const { data, error } = useSWR(gql, (query) => graphQLClient.request(query));
const loading = !data
return { data, error, loading }
}
first, keep consistent your promises, use async/await or choose for chaining it with then, but not both. if you await acquireToken() you can store its value in variable. also, if you choose for async/await wrap your code with a try/catch block, for handling errors properly:
export const useApi = async (gql: string) => {
const { acquireToken } = useAuth()
try {
const res = await acquireToken()
graphQLClient.setHeader('authorization', `Bearer ${res.accessToken}`)
const { data, error } = useSWR(gql, (query) => graphQLClient.request(query))
const loading = !data
return { data, error, loading }
catch(error) {
// handle error
}
}

Resources