React Native AsyncStorage fetches data after rendering - react-native

I am using AsyncStorage in ComponentWillMount to get locally stored accessToken, but it is returning the promise after render() function has run. How can I make render() wait until promise is completed? Thank you.

You can't make a component wait to render, as far as I know. What I've done in the app I'm working on is to add a loading screen until that promise from AsyncStorage resolves. See the examples below:
//
// With class component syntax
//
import React from 'react';
import {
AsyncStorage,
View,
Text
} from 'react-native';
class Screen extends React.Component {
state = {
isLoading: true
};
componentDidMount() {
AsyncStorage.getItem('accessToken').then((token) => {
this.setState({
isLoading: false
});
});
},
render() {
if (this.state.isLoading) {
return <View><Text>Loading...</Text></View>;
}
// this is the content you want to show after the promise has resolved
return <View/>;
}
}
//
// With function component syntax and hooks (preferred)
//
import React, { useEffect } from 'react';
import {
AsyncStorage,
View,
Text
} from 'react-native';
const Screen () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
AsyncStorage.getItem('accessToken').then((token) => {
setIsLoading(false);
});
}, [])
if (isLoading) {
return <View><Text>Loading...</Text></View>;
}
// this is the content you want to show after the promise has resolved
return <View/>;
}
Setting the isLoading property in state will cause a re-render and then you can show the content that relies on the accessToken.
On a side note, I've written a little library called react-native-simple-store that simplifies managing data in AsyncStorage. Hope you find it useful.

Based on react-native doc, you can do something like this:
import React, { Component } from 'react';
import {
View,
} from 'react-native';
let STORAGE_KEY = '#AsyncStorageExample:key';
export default class MyApp extends Component {
constructor(props) {
super(props);
this.state = {
loaded: 'false',
};
}
_setValue = async () => {
try {
await AsyncStorage.setItem(STORAGE_KEY, 'true');
} catch (error) { // log the error
}
};
_loadInitialState = async () => {
try {
let value = await AsyncStorage.getItem(STORAGE_KEY);
if (value === 'true'){
this.setState({loaded: 'true'});
} else {
this.setState({loaded: 'false'});
this._setValue();
}
} catch (error) {
this.setState({loaded: 'false'});
this._setValue();
}
};
componentWillMount() {
this._loadInitialState().done();
}
render() {
if (this.state.loaded === 'false') {
return (
<View><Text>Loading...</Text></View>
);
}
return (
<View><Text>Main Page</Text></View>
);
}
}

you can use react-native-easy-app that is easier to use than async storage.
this library is great that uses async storage to save data asynchronously and uses memory to load and save data instantly synchronously, so we save data async to memory and use in app sync, so this is great.
import { XStorage } from 'react-native-easy-app';
import { AsyncStorage } from 'react-native';
const initCallback = () => {
// From now on, you can write or read the variables in RNStorage synchronously
// equal to [console.log(await AsyncStorage.getItem('isShow'))]
console.log(RNStorage.isShow);
// equal to [ await AsyncStorage.setItem('token',TOKEN1343DN23IDD3PJ2DBF3==') ]
RNStorage.token = 'TOKEN1343DN23IDD3PJ2DBF3==';
// equal to [ await AsyncStorage.setItem('userInfo',JSON.stringify({ name:'rufeng', age:30})) ]
RNStorage.userInfo = {name: 'rufeng', age: 30};
};
XStorage.initStorage(RNStorage, AsyncStorage, initCallback);

React-native is based on Javascript which does not support blocking functions.Also this makes sense as we don't want the UI to get stuck or seem unresponsive.
What you can do is handles this in the render function. i.e Have a loading screen re-render it as you as you get the info from the AsyncStorage

Related

Context API dispatch not called with onEffect while using expo-splash-screen

When I am trying to use the dispatch function recieved with the useContext hook I cannot get the change the content of the data inside the context. It looks like as if the call wasn't even made, when I try to log something inside the conext's reducer it doesn't react. When I try to call it from other components, it works just fine.
Sorry if it's not clean enough, I'm not too used to ask around here, if there's anything else to clarify please tell me, and I'll add the necessary info, I just don't know at the moment what could help.
import { QueryClient, QueryClientProvider } from "react-query";
import LoginPage from "./src/pages/LoginPage";
import { UserDataContext, UserDataProvider } from "./src/contexts/UserData";
import { useState } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { useContext } from "react";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { useCallback } from "react";
import { UserData } from "./src/interfaces";
SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient();
export default function App() {
const [appReady, setAppReady] = useState<boolean>(false);
const { loggedInUser, dispatch } = useContext(UserDataContext);
useEffect(() => {
async function prepare() {
AsyncStorage.getItem("userData")
.then((result) => {
if (result !== null) {
console.log(loggedInUser);
const resultUser: UserData = JSON.parse(result);
dispatch({
type: "SET_LOGGED_IN_USER",
payload: resultUser,
});
new Promise((resolve) => setTimeout(resolve, 2000));
}
})
.catch((e) => console.log(e))
.finally(() => setAppReady(true));
}
if (!appReady) {
prepare();
}
}, []);
const onLayoutRootView = useCallback(async () => {
if (appReady) {
await SplashScreen.hideAsync();
}
}, [appReady]);
if (!appReady) {
return null;
}
return (
<>
<UserDataProvider>
<QueryClientProvider client={queryClient}>
<LoginPage onLayout={onLayoutRootView} />
</QueryClientProvider>
</UserDataProvider>
</>
);
}
I'm thinking I use the context hook too early on, when I check the type of the dispatch function here it says it's [Function dispatch], and where it works it's [Function bound dispatchReducerAction].
I think the problem might come from me trying to call useContext before the contextprovider could render, but even when I put the block with using the dispatch action in the onLayoutRootView part, it didn't work.

How Redirect to Login if page is protected and the user is not signed in?

In my App I have some public screens that are accessible even if the user is not logged in, and some screens are protected (you must be logged in to access them).
My solution to the problem is to check the component willFocus Listener and if not logged in, the user should be redirected to the loginPage.
export async function ProtectRoute(navigation){
//if page will enter and the user is not authenticated return to login
navigation.addListener(
'willFocus',
async () => {
let token = await getTokenAsync();
if(!token){
navigation.navigate('Login');
}
})
}
In my screen I Call this function in ComponentWillMount lifecycle.
The issue is that it takes like a second to verify the token and the page is displayed briefly.
How can I make it so that he goes directly to the Login Page without that lag ?
I wrote a quick example below. You can examine and use it.
import React, { Component } from "react";
import { Text, View } from "react-native";
const withAuth = WrappedComponent => {
class AuthenticationScreen extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
props.navigation.addListener("willFocus", async () => {
await this.checkAuth();
});
}
remoteReuqest = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
};
checkAuth = async () => {
const result = await this.remoteReuqest();
if (result) {
this.setState({
isAuthenticated: true
});
} else {
this.props.navigation.navigate("Login");
}
};
render() {
if (!this.state.isAuthenticated) {
return <Text>Waiting...</Text>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthenticationScreen;
};
export default withAuth;
You can use it as follows.
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import withAuth from "./withAuth";
class ContactScreen extends Component {
render() {
return (
<View>
<Text> Contact Screen </Text>
</View>
);
}
}
const styles = StyleSheet.create({});
const extendedComponent = withAuth(ContactScreen);
extendedComponent.navigationOptions = {
title: "Contact"
};
export default extendedComponent;
The issue is that it takes like a second to verify the token and the page is displayed briefly.
The reason is because reading/writing from/to AsyncStorage is an asychronous operation.
In my screen I Call this function in ComponentWillMount lifecycle.
I suggest you to not use ComponentWillMount lifecycle because it's deprecated and it will be removed from React (https://reactjs.org/docs/react-component.html#unsafe_componentwillmount)
After this introduction, now i show you how I have achieved this in my app: CONTEXT API! (https://reactjs.org/docs/context.html)
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How to implement context api:
the context will be the 'state' of your App.js file. You root App.js will be the provider of the context, while other views which will need the context are called the consumers of the context.
First of all, you need to create a 'skeleton' of your context into a separate file, something like this:
// AuthContext.js
import React from 'react'
const AuthContext = React.createContext({
isLogged: false,
login: () => {},
logout: () => {}
})
export default AuthContext
Your App.js will import, contain and initialize the context:
// App.js
// all necessary imports
import AuthContext from '....'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
isAuth: false,
login: this.login,
logout: this.logout
}
login = async userData => {
// do your login stuff and then
this.setState({ isAuth: true })
}
logout = async () => {
// do your logout stuff and then
this.setState({ isAuth: false })
}
async ComponentDidMount () {
// check the asyncStorage here and then, if logged:
this.setState({ isAuth: true })
}
render () {
return (
<AuthContext.Provider value={this.state}>
<AppContainer />
</AuthContext.Provider>
)
}
Then, into the View contained into AppContainer, you could access context like this:
import AuthContext from '.....'
// all necessary imports
export default class YourView extends React.Component<Props, State> {
constructor (props) {
super(props)
this.props = props
this.state = { ... }
}
// THIS IS IMPORTANT
static contextType = AuthContext
// with this, you can access the context through 'this.context'
ComponentDidMount () {
if (!this.context.isAuth) this.props.navigation.navigate('login')
}
Advantages of this approach:
Checking a boolean is so fast that you will not notice a blank screen.
Sharing an Authentication Context everywhere in you app
Making the access to asyncstorage only the first time that app mounts and not everytime you need to check if the user is logged
Sharing methods to login/logout everywhere in your app

React-Native Flash-Message with Mobx and React-navigation

I'm trying to use react-native-flash-message to provide little toasts in my app, and I am supposed to add the flash message to my root view. I cannot figure out where that is in my app. I used examples to get up and running with mobx/react-navigation/react-native, and so my app.js actually just exports an index.js that looks like this:
import React from 'react';
import { createRootNavigator } from './router';
import { isSignedIn } from './auth';
import { Font, SplashScreen } from 'expo';
import { library } from '#fortawesome/fontawesome-svg-core';
import { faCheckSquare, faCoffee, faHome } from '#fortawesome/free-solid-svg-icons';
import { configure } from 'mobx';
import { Provider } from 'mobx-react';
import _ from 'lodash';
import { RootStore } from './stores/RootStore';
import { getDecoratedStores } from './stores/util/store-decorator';
import { AsyncStorage } from 'react-native';
import FlashMessage from "react-native-flash-message";
configure({ enforceActions: 'observed' });
const rootStore = new RootStore();
const stores = getDecoratedStores(rootStore);
//Library of Icons
library.add(faCheckSquare, faCoffee, faHome);
export default class App extends React.Component<
{},
{ checkedSignIn: boolean; signedIn: boolean; loaded: boolean }
> {
constructor(props: any) {
super(props);
this.state = {
signedIn: false,
checkedSignIn: false,
loaded: false,
};
}
componentWillMount() {
this._loadFontsAsync();
}
_loadFontsAsync = async () => {
await Font.loadAsync({ robotoBold: require('../app/uiComponents/fonts/Roboto-Bold.ttf') });
await Font.loadAsync({
robotoRegular: require('../app/uiComponents/fonts/Roboto-Regular.ttf'),
});
this.setState({ loaded: true });
};
componentDidMount() {
isSignedIn()
.then(res => {
SplashScreen.hide();
this.setState({ signedIn: res as boolean, checkedSignIn: true });
})
.catch(err => {console.log(err); alert('Error')});
}
render() {
const { checkedSignIn, signedIn } = this.state;
// If we haven't checked AsyncStorage yet, don't render anything (better ways to do this)
if (!checkedSignIn) {
return null;
}
AsyncStorage.getItem('auth-demo-key').then((data) => console.log("Async Storage: " + data));
const Layout = createRootNavigator(signedIn);
return (
<Provider {...stores}>
<Layout />
</Provider>
);
}
}
Can anyone help me figure out how to add my flash message in this return statement? I tried wrapping the provider in a view, but that failed and crashed my app, same with adding it within the provider (a view), also tried just adding flash message here in the provider, but that failed, too. Can anyone help?

Persisting data between app launches with Expo & React Native

Here's my App.js, everything else is as standard/simple as I can get it.
import React from 'react';
import { AsyncStorage, Text, View } from 'react-native';
export default class App extends React.Component {
render() {
console.log("Fetching data")
AsyncStorage.getItem('#MySuperStore:key', (value) => {
console.log("Fetched data: ", value)
if(value == null) {
console.log("Writing data!")
AsyncStorage.setItem('#MySuperStore:key', 'data', () => {
console.log("Wrote data!")
})
}
})
return(
<View>
<Text>Hello, ReplIt</Text>
</View>
);
}
}
The fetched value is always null.
I've tried this both locally and on ReplIt. In all cases, the data does not persist across app loads; I always see:
Fetching data
Fetched data: null
Writing data!
Wrote data!
What am I doing wrong? Do I have an incorrect assumption about how Expo interacts with the persistent storage? AFAIK, AsyncStorage is supposed to save stuff to the device; so I can close and re-open the app and have the data persist.
UPD: i just realized your code worked as expected... probably it is replit issue as mentioned in comment.
Avoid any requests and async calls in render method, because it could be called may times depending on how props or state changing. Better put all related code into componentDidMount as it is recommended in documentation. It will be called only once when component mounted.
Not sure why your code dodnt worked for you, callbacks are allowed for AsyncStorage, however wait works just fine for me:
import React from "react";
import { AsyncStorage, Text, View } from "react-native";
export default class App extends React.Component {
constructor() {
super();
this.state = {
storedValue: null
};
}
async componentDidMount() {
let storedValue = await AsyncStorage.getItem("#MySuperStore:key");
console.log("Fetched data: ", storedValue);
if (storedValue == null) {
console.log("Writing data!");
storedValue = await AsyncStorage.setItem("#MySuperStore:key", "data");
}
this.setState({
storedValue
});
}
render() {
const { storedValue } = this.state;
return (
<View>
<Text>Hello, ReplIt</Text>
<Text>This is Stored Value, '{storedValue}'</Text>
</View>
);
}
}
give these a try. AsyncStorage is a Javascript Promise based method.
AsyncStorage.getItem('#MySuperStore:key')
.then(value => console.log(value))
or
value = await AsyncStorage.getItem('#MySuperStore:key');
console.log(value);

How to detect first launch in react-native

What is a good way to detect the first and initial launch of an react-native app, in order to show an on-boarding/introductory screen ?
Your logic should follow this:
class MyStartingComponent extends React.Component {
constructor(){
super();
this.state = {firstLaunch: null};
}
componentDidMount(){
AsyncStorage.getItem("alreadyLaunched").then(value => {
if(value === null){
AsyncStorage.setItem('alreadyLaunched', 'true'); // No need to wait for `setItem` to finish, although you might want to handle errors
this.setState({firstLaunch: true});
}
else{
this.setState({firstLaunch: false});
}}) // Add some error handling, also you can simply do this.setState({fistLaunch: value === null})
}
render(){
if(this.state.firstLaunch === null){
return null; // This is the 'tricky' part: The query to AsyncStorage is not finished, but we have to present something to the user. Null will just render nothing, so you can also put a placeholder of some sort, but effectively the interval between the first mount and AsyncStorage retrieving your data won't be noticeable to the user.
}else if(this.state.firstLaunch === 'true'){
return <FirstLaunchComponent/>
}else{
return <NotFirstLaunchComponent/>
}
}
Hope it helps.
I made some adjustments to martinarroyo's suggestion. AsyncStorage.setItem should set a string value and not a bool.
import { AsyncStorage } from 'react-native';
const HAS_LAUNCHED = 'hasLaunched';
function setAppLaunched() {
AsyncStorage.setItem(HAS_LAUNCHED, 'true');
}
export default async function checkIfFirstLaunch() {
try {
const hasLaunched = await AsyncStorage.getItem(HAS_LAUNCHED);
if (hasLaunched === null) {
setAppLaunched();
return true;
}
return false;
} catch (error) {
return false;
}
}
This function can then be imported wherever you need it. Note that you should render null (or something else clever) while waiting for the async function to check AsyncStorage.
import React from 'react';
import { Text } from 'react-native';
import checkIfFirstLaunch from './utils/checkIfFirstLaunch';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isFirstLaunch: false,
hasCheckedAsyncStorage: false,
};
}
async componentWillMount() {
const isFirstLaunch = await checkIfFirstLaunch();
this.setState({ isFirstLaunch, hasCheckedAsyncStorage: true });
}
render() {
const { hasCheckedAsyncStorage, isFirstLaunch } = this.state;
if (!hasCheckedAsyncStorage) {
return null;
}
return isFirstLaunch ?
<Text>This is the first launch</Text> :
<Text>Has launched before</Text>
;
}
}
In 2022: Note that AsyncStorage from react-native is already deprecated.
Use react-native-async-storage/async-storage instead.
Custom hook:
import React, { useState } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
async function checkIfFirstLaunch() {
try {
const hasFirstLaunched = await AsyncStorage.getItem("#usesr_onboarded");
if (hasFirstLaunched === null) {
return true;
}
return false;
} catch (error) {
return false;
}
}
const useGetOnboardingStatus = () => {
const [isFirstLaunch, setIsFirstLaunch] = useState(false);
const [isFirstLaunchIsLoading, setIsFirstLaunchIsLoading] = useState(true);
React.useEffect(async () => {
const firstLaunch = await checkIfFirstLaunch();
setIsFirstLaunch(firstLaunch);
setIsFirstLaunchIsLoading(false);
}, []);
return {
isFirstLaunch: isFirstLaunch,
isLoading: isFirstLaunchIsLoading,
};
};
export default useGetOnboardingStatus;
Usage:
import useGetOnboardingStatus from "../../utils/useGetOnboardingStatus";
const { isFirstLaunch, isLoading: onboardingIsLoading } =
useGetOnboardingStatus();
As Eirik Fosse mentioned: You can use onboardingIsLoading to return null while waiting for the response.