react native setState inside an async asyncStorage function

406 views Asked by At

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>}
1

There are 1 answers

3
FutureJJ On

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.