Redux-persist: PersistGate in react-redux doesn't wait the store to be rehydrated in a RN app

Created on 12 Mar 2019  路  5Comments  路  Source: rt2zz/redux-persist

I have two navigators: AppNavigatorand BottomBarNavigator, the first one is for the authentication flow, the second one is when the user is authenticated.

These navigators are inside a createSwitchNavigator, and I want the right navigator to be chosen based on a loggedIn variable I store inside my store/auth.

_Router.js_

const MyNavigator = (!loggedIn) ? createSwitchNavigator({
    AppNavigator: AppNavigator,
    BottomBarNavigator: BottomBarNavigator,
}) : createSwitchNavigator({
    BottomBarNavigator: BottomBarNavigator,
    AppNavigator: AppNavigator,
});  

I persist my store/auth variable inside redux-persist.

_Store.js_

import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';

const persistConfig = {
    key: 'root',
    storage: storage,
    whitelist: ['auth']
};

const pReducer = persistReducer(persistConfig, reducers);

export const store = createStore(pReducer, undefined, applyMiddleware(ReduxThunk, logger));
export const persistor = persistStore(store);

In my App.js, I wrap my navigator with PersistGate, but although the state is rehydrated after some milliseconds, the Navigator is rendered before and so is always chosen the AppNavigator.

_App.js_

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

    render(){
        return (
            <Provider store={store}>
                <PersistGate 
                    loading={null} 
                    onBeforeLift={this.onBeforeLift}
                    persistor={persistor}>

                    <MyNavigator ref={navigatorRef => {NavigationService.setTopLevelNavigator(navigatorRef);}}/>

                </PersistGate>
            </Provider>

Any suggestions to make it work properly?

Most helpful comment

Unlike you showed above, we just have only one navigator for this. Even react-navigation says that "dynamic navigation" is a bit flacky, therefor we are using a switch-navigator, and switch scenes after store was rehydrated. Here some example how we manage this:

import React from 'react';
// ....
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { PersistGate } from 'redux-persist/integration/react';
import { Provider } from 'react-redux';

import { store, persistor } from './redux/store';

const AppNavigationContainer = createAppContainer(
    createSwitchNavigator({
        ApplicationLoadingScreen: {
            screen: ApplicationLoadingScreen
        },
        UnauthedScreen: {
            screen: LoginStack // to be defined elsewhere
        },
        AuthedScreen: {
            screen: InsideStack // to be defined elsewhere
        }
    }, {
        initialRouteName: 'ApplicationLoadingScreen'
    })
);

export default class App extends React.Component {

    /**
     * registers the passed reference into the internal handle-instance, its not stored
     * as part of the redux-state, so we do not have to wait for re-hydration procedure
     */
    registerNavigator = ref => {
        store.dispatch({
            type: 'REGISTER_SCENE_NAVIGATOR_HANDLE',
            payload: ref
        });
    }

    runInitialAppStartActions = () => {
        // store now has been rehydrated

        // check inside store if we have some auth token as marker for the user being "logged in"
        if (!!store.getState().auth.token) {
            // TODO do something to check that token being still valid
            store.dispatch({
                type: 'NAVIGATE_TO_SCENE',
                payload: 'AuthedScreen'
            });
        } else {
            // we now know that user is not yet logged in, so switch to login-screen
            store.dispatch({
                type: 'NAVIGATE_TO_SCENE',
                payload: 'UnauthedScreen'
            });
        }
    }
    render() {
        return (
            <Provider store={store}>
                <PersistGate loading={<ApplicationLoadingScreen />} persistor={persistor} onBeforeLift={this.runInitialAppStartActions}>
                    <AppNavigationContainer ref={navigatorRef => { this.registerNavigator(navigatorRef); }}/>
                </PersistGate>
            </Provider>
        );
    }
}

Please keep in mind, that we have a way to navigate via redux-dispatch actions.

All 5 comments

You might want to have some kind of "loading"-scene as part of loading=. Did you try using anything else than null?

import React from 'react';
import { View, ActivityIndicator, SafeAreaView } from 'react-native';

export class ApplicationLoadingScreen extends React.Component {
    render() {
        return (
                <View style={{flex: 1, justifyContent: 'center'}}>
                    <ActivityIndicator size="large" color="#000000" />
                </View>
        );
    }
}

We are using react-navigation and are managing that "already logged in"-switch as part of the onBeforeLift-method. As the first scene inside a SwitchNavigator-stack we included that same loading-scene as initialRouteName. This makes the app starting with the loading-scene (shown through the PersistGate), then being shown as part of the SwitchNavigator-stack, then switched to the login-screen or the "inside"-part afterwards.

Maybe this helps a bit.

inside

Thank you for the answer.
Unfortunately I already tried to use a view as the loading view, but nothing change.
How do yo change afterwards?

Unlike you showed above, we just have only one navigator for this. Even react-navigation says that "dynamic navigation" is a bit flacky, therefor we are using a switch-navigator, and switch scenes after store was rehydrated. Here some example how we manage this:

import React from 'react';
// ....
import { createAppContainer, createSwitchNavigator } from 'react-navigation';
import { PersistGate } from 'redux-persist/integration/react';
import { Provider } from 'react-redux';

import { store, persistor } from './redux/store';

const AppNavigationContainer = createAppContainer(
    createSwitchNavigator({
        ApplicationLoadingScreen: {
            screen: ApplicationLoadingScreen
        },
        UnauthedScreen: {
            screen: LoginStack // to be defined elsewhere
        },
        AuthedScreen: {
            screen: InsideStack // to be defined elsewhere
        }
    }, {
        initialRouteName: 'ApplicationLoadingScreen'
    })
);

export default class App extends React.Component {

    /**
     * registers the passed reference into the internal handle-instance, its not stored
     * as part of the redux-state, so we do not have to wait for re-hydration procedure
     */
    registerNavigator = ref => {
        store.dispatch({
            type: 'REGISTER_SCENE_NAVIGATOR_HANDLE',
            payload: ref
        });
    }

    runInitialAppStartActions = () => {
        // store now has been rehydrated

        // check inside store if we have some auth token as marker for the user being "logged in"
        if (!!store.getState().auth.token) {
            // TODO do something to check that token being still valid
            store.dispatch({
                type: 'NAVIGATE_TO_SCENE',
                payload: 'AuthedScreen'
            });
        } else {
            // we now know that user is not yet logged in, so switch to login-screen
            store.dispatch({
                type: 'NAVIGATE_TO_SCENE',
                payload: 'UnauthedScreen'
            });
        }
    }
    render() {
        return (
            <Provider store={store}>
                <PersistGate loading={<ApplicationLoadingScreen />} persistor={persistor} onBeforeLift={this.runInitialAppStartActions}>
                    <AppNavigationContainer ref={navigatorRef => { this.registerNavigator(navigatorRef); }}/>
                </PersistGate>
            </Provider>
        );
    }
}

Please keep in mind, that we have a way to navigate via redux-dispatch actions.

Hi @aboscus,
we do have similar problem and after debugging I found that this workaround (passing children as a function) works.

The problem is, that React "executes the children" despite the fact that there is the a condition in the render method of PersistGate.

render() {
  return (
    <Provider
      store={store}
      children={bootstrapped => {
        if (!bootstrapped) {
          return null
        }

        return (
          <PersistGate
            loading={null}
            onBeforeLift={this.onBeforeLift}
            persistor={persistor}
          >
            <MyNavigator
              ref={navigatorRef => {
                NavigationService.setTopLevelNavigator(navigatorRef)
              }}
            />
          </PersistGate>
        )
      }}
    />
  )
}

Adding empty function seems to fix the issue for me.

const onBeforeLift = () => ({});
...
<PersistGate loading={loadingComponent} persistor={persistor} onBeforeLift={onBeforeLift}>
Was this page helpful?
0 / 5 - 0 ratings