Using a state variable in Axios put request in React Native - react-native

I am modifying a state variable (setTokens(tokens - 1)) in React native and then using the modified variable in an axios PUT request (library_tokens: tokens).
The problem is, the put request is successful, but it puts the old value of the variable instead of the modified one in the database after the PUT request.
The following is the code:
const [tokens,setTokens] = useState(0)
const lendBook = book => {
// Add book to the IssueReturn collection in the database
issuesApi
.post('/bookissue', {
booksIssued: book._id,
image: book.image,
user: context.stateUser.user.userId,
})
.then(response => {
if (response.status === 200) {
// Decrement user's library token
setTokens(tokens - 1);
// Modify the user's library tokens
userProfileApi
.put(
`/${context.stateUser.user.userId}`,
{
library_tokens: tokens,
},
{
headers: {Authorization: `Bearer ${jwtToken}`},
},
)
.then(response => {
console.log('tokens', tokens);
});
}
})
.catch(err => {
console.log(err);
});
};

You can use useEffect like this.
useEffect(() => {
console.log(tokens) // do something after state has updated
}, [tokens])
After state updation the useEffect will be called, so you can call userProfileApi inside useEffect.

setTokens it not a synchron function. code executed afterwards will not have the updated state. either use a local variable to store the result of token - 1 or make use of the useEffect hook.

Related

react native setState inside an async asyncStorage function

I am using the expo-auth-session package to make a request to the Spotify API to get access tokens, then saving to AsyncStorage.
A save function that stores the token in AsyncStorage:
const save = async (token) => {
try{
AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
A getItem function that gets the access token value from AsyncStorage, and sets that value to the spotifyAccessToken state
const [spotifyAccessToken, setSpotifyAccessToken] = useState('');
const getItem = async () => {
try{
const token = await AsyncStorage.getItem('access_token')
setSpotifyAccessToken(token);
}
catch(error){
console.log(error)
}
}
Using the useAuthRequest from expo-auth-session to make a request to Spotify API, the request code below works.
const discovery = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: "https://accounts.spotify.com/api/token"
};
const [request, response, promptAsync] = useAuthRequest({
// responseType: ResponseType.Token,
responseType: 'code',
clientId: client_id,
//clientSecret: client_secret,
scopes: ['user-read-recently-played'],
usePKCE: false,
redirectUri: REDIRECT_URI
}, discovery)
useEffect(() => {
if (response?.type === 'success'){
//console.log(response.params.code);
axios.request({
method: 'POST',
url: 'https://accounts.spotify.com/api/token',
headers: {
'content-type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${new Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`,
},
data: {
grant_type: 'authorization_code',
code: response.params.code,
redirect_uri: REDIRECT_URI
}
}).then(res => {
save(res.data.access_token);
}).catch(err => {console.log(err)})
}
},
[response]);
A button that triggers the user to login using Spotify account, after authenticating, it redirects back to this component screen, however, I want the text below the button to be displayed from "Loading..." to the spotifyAccessToken immediately after it redirects to the component screen, but it wouldn't. After I re-run my application, the token is displayed, which means it was successfully stored in AsyncStorage, but didn't update the state immediately. How can solve this? Thanks.
const [spotifyAccessToken, setSpotifyAccessToken] = useState(null);
useEffect(()=>{
//clearTokens();
// console.log('storage: ' + getValueForfor('access_token'))
// console.log('state: ' + spotifyAccessToken)
getItem()
}, [spotifyAccessToken])
<Button title='login to spotify' onPress={() => promptAsync()}/>
{spotifyAccessToken != '' ? <Text> {spotifyAccessToken} </Text> : <Text> Loading... </Text>}
This might be happening if you are redirecting to the component with getItem too early: before the AsyncStorage is done saving the token. Due to this, at the initial render of the component(with getItem), AsyncStorage.getItem might be getting the old value of access_token and not the updated one.
To possibly fix this issue, try redirecting to the next component only after AsyncStorage.setItem promise is resolved completely. Something like this:
This is how your save function should look like: it should return a Promise value:
const save = async (token) => {
try{
await AsyncStorage.setItem('access_token', token)
}
catch(error){
console.log(error)
}
}
And redirect to the next component after the save return promise value is resolved:
...
).then(async (res) => {
await save(res.data.access_token);
// Redirect here, after save is resolved
})...
Answering the question you asked in the comments to this answer:
it's not working still, you said that the save function should return a promise value, where in the code should I put it
Using await for a Promise makes the function wait till the promise is resolved (here when setItem is done). You do not need to explicitly return a Promise value from the async function in this case. If you do not use await, the function will return prematurely (without waiting for the setItem promise). The setItem promise will still resolve concurrently just that your code wouldn't be able to know when it is resolved.
By using await for setItem here, you just propagate promise resolution to the calling function(here in the then(res => {...}) block).
In the then(res => {}) block you can either use await to wait for the save to complete before executing the next statement. Or use then/catch and add the next statement to execute after save is done in the then block.
Edit: As OP mentioned in the comments below, the redirection to the next component is done automatically. Well, in this case, setting the value in AsyncStorage and immediately getting it in the next component might not work as expected because of the above-mentioned reason.
First, you will need to check if the auto-redirection to the next component is really done after the axios request completes or before it, i.e. as soon as response?.type === 'success'. I am unable to understand why you have made the axios request after you already got success from auth request
If the redirection is happening before the axios request call then you might be able to access the token in the success condition itself:
if (response?.type === 'success'){
// Check if the token is available here?
console.debug(`Response = ${JSON.stringify(response)}`);
// If token is available here itself, then why is the axios request required?
// Save the token here itself...
// Use SessionStorage if required, implementation explained below in the answer
...
}
If you confirmed the above and the auto-redirection is really done after the axios request and NOT after response?.type === 'success' then:
You could use react-native-session-storage as volatile storage to set and get the token in the same session and use AsyncStorage in parallel to it to set and get the token in/from persistent memory.
So, the save function will look like this with SessionStorage:
import SessionStorage from 'react-native-session-storage';
...
const save = async (token) => {
try{
// Set token in SessionStorage as well to allow access to the value immediately
SessionStorage.setItem(`access_token`, token);
// Store token to AsyncStorage to persist it when the app closes.
await AsyncStorage.setItem('access_token', token);
}
catch(error){
console.log(error)
}
}
And getItem function will look like this:
import SessionStorage from 'react-native-session-storage';
...
const getItem = async () => {
try{
let token = await AsyncStorage.getItem('access_token');
// If the token is not yet set in Async Storage, fetch it from Session Storage
// If it's set in Async Storage, use that value
if(!token) // If it's null
token = SessionStorage.getItem('access_token');
setSpotifyAccessToken(token);
// Don't forget to clear both SessionStorage and AsyncStorage on logout!
}
catch(error){
console.log(error)
}
}
Why both storages?
AsyncStorage
-> to persist the token when the user re-opens the app.
SessionStorage
-> as an immediate way to R/W the value during the same session (gets cleared when the app closes).
Another solution:
Use ContextProvider, if your code structure allows it. Wrap the context over the next component to "listen" to token value state change from anywhere in the children components.

Update Next.js to React 18 breaks my API calls using next-auth

This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233

Unable to set useState variable in async method and console log it

Im working with my friends on a app project and we find an issue many times when we tring to set a use state and the console log the variable, i've looking for a solution in the website and saw that the reason is that the usestate is an async awiat which means the variable that i set in the use state isn't immidatly set in it, so i tried many solution that i found in the websites but none of them work for me.
in the screenShot you can see that the json variable is in console log before and the set varaible doesn't show after the setActiveUser , any help?
Thanks!
If you want to do something upon setting state then your best bet is to use the useEffect hook from React and adding your state in the dependency array. This will then call the function in your useEffect every time the state changes. See below a rough example:
import { Text } from 'react-native';
const MyComponent = () => {
const [activeUser, setActiveUser] = useState(null);
useEffect(() => {
// This should log every time activeUser changes
console.log({ activeUser });
}, [activeUser]);
const fetchAuthentication = async user => {
var flag = false;
await fetch('/api/authUser/', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user),
})
.then(res => {
res.ok && (flag = true);
return res.json();
})
.then(json => {
if (flag) {
setActiveUser(json);
}
})
.catch(error => console.error(error));
return flag;
};
return <Text>Hi</Text>;
};
Full documentation: https://reactjs.org/docs/hooks-effect.html

React Native Doesn`t re-render after context state change

state changes after getting data from api. i can see that with console.log()
but doesn`t render data after update. there is my code fetch code :
useEffect(() => {
axios
.get(
'https://mylink.ngrok.io/channels/getUser',
).then((data)=>{
let array = [];
const promises = data.data.channels.map((channel)=>{
axios.get(`https://mylink.ngrok.io/channels/getChannel/${channel}`).then((resp)=>{
array.push(resp.data);
})
})
Promise.all(promises).then(()=>{
setonLineChannels(array);
});
})
}, []);
btw its in context.
I think Promise.all casue of that
thx
Where you declare promises, it's not going to wait for those API requests to complete. They're running even before you've called Promise.all(promises).
You can just put the code you've defined as promises inside the Promise.all() directly, then make sure you return the get request inside. And don't push to array. Your map will return an array of promises, and you can use the results array in the .then after Promise.all to update your state directly.
Here's what I mean:
useEffect(() => {
axios.get("https://mylink.ngrok.io/channels/getUser").then((data) => {
Promise.all(
data.data.channels.map(
(channel) =>
axios
.get(`https://mylink.ngrok.io/channels/getChannel/${channel}`)
.catch((err) => console.error(err)) // this allows an individual request to fail
)
).then((results) => setonLineChannels(results));
});
}, []);

React native fetch api call is not making the request

I am using an api call to get information for my app which I display to the user. The problem is that when I open the screen for the first time the app displays the information but when I go to a different screen and then comeback I dont see the information unless I restart the app.
This function makes the apiCall for me:
async function getOrders() {
var retrieveData = async () => {
try {
var value = await AsyncStorage.getItem("user");
var data = JSON.parse(value);
return data.user.email;
} catch (error) {
alert(error);
}
};
retrieveData().then((usr) => {
setUser(usr)
fetch(URL + "/api/order/quoted", {
method: "POST",
body: "user=" + usr,
headers: { "Content-type": "application/x-www-form-urlencoded" },
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.error === null) {
setOrders(responseJson.orders);
}
});
});
}
First I use the retriveData function to get the used id, based on that information is server to the user.
You are using react-navigation version 5, so you need to wrap your logic fetch data in useFocusEffect hook react navigation docs
import { useFocusEffect } from '#react-navigation/native';
useFocusEffect(
React.useCallback(() => {
getOrders()
}, [getOrders])
);
The problem can be solved in the following steps:
If you want the data fetched from your endpoint to be used even if you move to other screen use Redux.
If you use redux or not and want to fetch the api every time you open a specific screen then you need to add an onfocus listener. An example is here https://reactnavigation.org/docs/navigation-events/
class Profile extends React.Component {
componentDidMount() {
this._unsubscribe = navigation.addListener('focus', () => {
// do something
});
}