How to use React Native AsyncStorage with Redux?

15,462

Solution 1

You can conveniently use AsyncStorage alone OR redux to manage authentication state. Depends on which you are comfortable with. I will give you an example of both.

For AsyncStorage: Assuming you have authentication keys that is valid for 2 weeks only. You can take note when your user logs in and save the time. eg:

//LoginScreen
import { onSignIn } from '../actions/auth'; //I will describe the onSignInMethod below
import axios from 'axios'; //lets use axios. You may use fetch too.


export default class LoginScreen extends Component {


    //your code: state, static etc
    loginMethod = () => {
        const url = yourauthUrl;
        const payload = {
            email: this.state.email,
            password: this.state.password
        };
        axios.post(url, payload)
        .then((response) => {
            if (response.status == 200) {
                const dateOfLastLogin = new Date().getTime().toString(); //take note of the time the user logs in.
                AsyncStorage.setItem('dateOfLastLogin', dateOfLastLogin);
            }
        })
        .then(() => { 
            onSignIn() //onSignIn handles your sign in. See below.
            .then(() => this.props.navigation.navigate('AfterSignInPage'));
            })
            .catch(() => { // your callback if onSignIn Fails
            });
        })
        .catch((error) => { //your callback if axios fails
        });
    }

}

In ../actions/auth.js

import { AsyncStorage } from 'react-native';

export const onSignIn = () => AsyncStorage.setItem('auth_key', 'true');
//in LoginScreen we called this to set that a user has successfully logged in
//why is true a string? -- Because Asyncstorage stores only strings

export const onSignOut = () => AsyncStorage.multiRemove(['auth_key', 'dateOfLastLogin']);

//now lets create a method that checks if the user is logged in anytime
export const isSignedIn = () => {
    return new Promise((resolve, reject) => {
        AsyncStorage.multiGet(['auth_key', 'dateOfLastLogin'])
        .then((res) => {
            const userKey = res[0][1];
            const lastLoginDate = parseInt(res[1][1]);
            const today = new Date().getTime();
            const daysElapsed = Math.round(
                (today - lastLoginDate) / 86400000
                );
            if (userKey !== null && (daysElapsed < 14)) {
                resolve(true);
            } else {
                resolve(false);
            }
        })
        .catch((err) => reject(err));
    });
};

now we can import { isSignedIn } from '../actions/auth'; from any of our components and use it like this:

isSignedIn()
    .then((res) => {
        if (res) { 
            // user is properly logged in and the login keys are valid and less than 14 days 
        }
    })

////////////////////////////////////////////////////////////////////////////

If you want to use redux

Handling login in redux

In your types.js

//types.js
export const LOGGED_IN = 'LOGGED_IN';

In your redux actions

//loginActions.js
import {
    LOGGED_IN,
} from './types';

export function login() {
    let dateOfLastLogin = null;
    let isLoggedIn = 'false';
    AsyncStorage.multiGet(['auth_key', 'dateOfLastLogin'])
    .then((res) => {
        isLoggedIn = res[0][1];
        dateOfLastLogin = parseInt(res[1][1]);
    }); //note this works asynchronously so, this may not be a good approach
    return {
        type: LOGGED_IN,
        isLoggedIn, 
        dateOfLastLogin
    };
}

In your loginReducer

//LoginReducer.js
import {
    LOGGED_IN
} from '../actions/types';


const initialState = {
    userIsLoggedIn: false
};

export function loginReducer(state=initialState, action) {
    switch (action.type) {

        case LOGGED_IN:

            const userKey = action.isLoggedIn;
            const lastLoginDate = action.dateOfLastLogin;
            const today = new Date().getTime();
            const daysElapsed = Math.round(
                (today - lastLoginDate) / 86400000
                );
            let trulyLoggedIn = false;
            if (userKey !== null && (daysElapsed < 14)) {
                trulyLoggedIn = true;
            } else { trulyLoggedIn = false }
            return {
                userIsLoggedIn: trulyLoggedIn
            };

        default:
            return state;
    }
}

In your ./reducers/index.js

//reducers index.js
import { combineReducers } from 'redux';

import { loginReducer } from './LoginReducers';

const rootReducer = combineReducers({
    loggedIn: loginReducer
});

export default rootReducer;

In your store where you used redux-thunk, applyMiddleWare. Lets call it configureStore.js

//configureStore.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

export default function configureStore(initialState) {
    return createStore(
        rootReducer,
        initialState,
        applyMiddleware(thunk)
    );
}

In your App.js

//App.js
import { Provider } from 'react-redux';
import configureStore from './src/store/configureStore'; //where you configured your store
import { YourMainNavigator } from '../src/config/router'; //where your root navigator is

const store = configureStore();
export default class App extends Component<{}> {
    render() {
        return (
            <Provider store={store}>
                <YourMainNavigator />
            </Provider>
        );
    }
}

You should know you no longer need the isSignedIn method in your auth.js Your login method remains the same as outlined above in LoginScreen.

Now you can use redux to check the state of login like this:

import React, {Component} from 'react';
import {connect} from 'react-redux';

class MyComponent extends Component {
    someFunction() {
        if (this.props.loggedIn) {
            //do something
        }
    }
}
const mapStateToProps = (state) => {
    return {
        loggedIn: state.loggedIn.userIsLoggedIn
    };
}


export default connect(mapStateToProps)(MyComponent);

There should be a better way of using redux to manage login - better than what I outlined here. I think you can also use redux to manage your login state without using AsyncStorage. All you need to do is in your loginScreen, if the login functions returns a response.status == 'ok', you can dispatch an action to redux that logs the user in. In the example above, using asyncstorage you might only need to use redux to check if a user is logged in.

Solution 2

To persist redux state I recommend you redux-persist.

Installation:

npm i -S redux-persist

Usage:

First, configure redux store

// configureStore.js

import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native

import rootReducer from './reducers'

const persistConfig = {
  key: 'root',
  storage,
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

export default () => {
  let store = createStore(persistedReducer)
  let persistor = persistStore(store)
  return { store, persistor }
}

Then, wrap your root component with PersistGate

import { PersistGate } from 'redux-persist/integration/react'

// ... normal setup, create store and persistor, import components etc.

const App = () => {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <RootComponent />
      </PersistGate>
    </Provider>
  );
};

Solution 3

It is recommended that you use an abstraction on top of AsyncStorage instead of AsyncStorage directly for anything more than light usage since it operates globally. Redux-persist is that abstraction that goes on top of AsyncStorage. It provides a better way to store and retrieve more complex data (e.g. redux-persist has persistReducer(), persistStore()).

React native typescript implementation

storage.ts

import AsyncStorage from "@react-native-community/async-storage";
import { createStore, combineReducers } from "redux";
import { persistStore, persistReducer } from "redux-persist";

import exampleReducer from "./example.reducer";

const rootReducer = combineReducers({
  example: exampleReducer,
});

const persistConfig = {
  key: "root",
  storage: AsyncStorage,
  whitelist: ["example"],
};


// Middleware: Redux Persist Persisted Reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = createStore(persistedReducer);

// Middleware: Redux Persist Persister
let persistor = persistStore(store);

export { store, persistor };

App.tsx

import React from "react";
import { PersistGate } from "redux-persist/es/integration/react";
import { Provider } from "react-redux";

import RootNavigator from "./navigation/RootNavigator";
import { store, persistor } from "./store";

function App() {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <RootNavigator />
      </PersistGate>
    </Provider>
  );
}

export default App;
Share:
15,462
fun joker
Author by

fun joker

Updated on July 25, 2022

Comments

  • fun joker
    fun joker almost 2 years

    I have made login and logout actions and userReducer. How can I integrate AsyncStorage with Redux? I am using Redux Thunk as a middleware.

    I am able to implement login and logout using internal state variable but I am not able to understand how to break it down into action and reducer as well as make use of AsyncStorage for storing accessToken.

    Original Code:

    _onLogin = () => {
        auth0.webAuth
          .authorize({
            scope: 'openid profile',
            audience: 'https://' + credentials.domain + '/userinfo'
          })
          .then(credentials => {
            this.setState({ accessToken: credentials.accessToken });
          })
          .catch(error => console.log(error));
      };
    
      _onLogout = () => {
        if (Platform.OS === 'android') {
          this.setState({ accessToken: null });
        } else {
          auth0.webAuth
            .clearSession({})
            .then(success => {
              this.setState({ accessToken: null });
            })
            .catch(error => console.log(error));
        }
      };
    

    loginAction.js:

       import { LOGIN_USER } from './types';
    import Auth0 from 'react-native-auth0';
    
    var credentials = require('./auth0-credentials');
    const auth0 = new Auth0(credentials);
    
    export const loginUser = () => dispatch => {
        auth0.webAuth
        .authorize({
          scope: 'openid profile',
          audience: 'https://' + credentials.domain + '/userinfo'
        })
        .then(credentials =>
            dispatch({
                type: LOGIN_USER,
                payload: credentials.accessToken
            })
        )
        .catch(error => console.log(error));
    }
    

    logoutAction.js:

           import { LOGOUT_USER } from './types';
    import Auth0 from 'react-native-auth0';
    
    var credentials = require('./auth0-credentials');
    const auth0 = new Auth0(credentials);
    
    export const logoutUser = () => dispatch => {
    
            auth0.webAuth
              .clearSession({})
              .then(success => 
                    dispatch({
                        type: LOGOUT_USER,
                        payload: null
                    })
              )
              .catch(error => console.log(error));
    }
    

    userReducer.js:

      import { LOGIN_USER, LOGOUT_USER } from '../actions/types';
    
    const initialState = {
        accessToken: null
    }
    
    export default function (state = initialState, action) {
        switch (action.type) {
    
            case LOGIN_USER:
    
                _storeData = async () => {
                    try {
                        await AsyncStorage.setItem('accessToken', action.payload);
                    } catch (error) {
                        console.log(error)
                    }
                }
    
                return {
                   ...state,
                   accessToken:action.payload
                };
    
            case LOGOUT_USER:
    
                _removeData = async (accessToken) => {
                    try {
                        await AsyncStorage.removeItem(accessToken);
                    } catch (error) {
                        console.log(error)
                    }
                }    
    
                return {
                    ...state,
                    accessToken:action.payload
                };
    
            default:
                return state;
        }
    }
    

    I am new to Redux so I tried converting original code into actions and reducers but I am not sure whether I have implemented AsyncStorage in userReducer.js correctly?

  • fun joker
    fun joker over 5 years
    Is it possible to use asyncstorage with redux ? I am trying to implement it using asyncstorage
  • fun joker
    fun joker over 5 years
    I am trying to split my original code into actions and reducer but I am not able to use asyncstorage with it.
  • Soroush Chehresa
    Soroush Chehresa over 5 years
    @funjoker The storage defaults to ‍‍AsyncStorage for react-native.
  • fun joker
    fun joker over 5 years
    check my updated loginAction.js and lougoutAction.js
  • fun joker
    fun joker over 5 years
    In logoutAction.js should it be -> payload: null or payload: success.null I want to set accessToken as null after logout button is clicked
  • fun joker
    fun joker over 5 years
    I am getting access token when I implement original code given in question. So as far as I understood you are saying that don't use asyncstorage and redux simultaneously. Only redux or only asyncstorage can solve the problem am I right ?
  • fun joker
    fun joker over 5 years
    I was following this tutorial -> snack.expo.io/@react-navigation/auth-flow
  • LordKiz
    LordKiz over 5 years
    You can use asyncstorage alone. You can use asyncstorage with redux. Or you can use only redux. For the access token u get during login, you can use asyncstorage to store that. Including client, expiry and uid also if you receive those too.