Search code examples
javascriptreact-nativereduxredux-persist

redux-persists not working with react native app


Issue

The store is not persisted or somehow is reset when relaunching the application

Expected behaviour

The app has basically just a login form in which some redux actions are triggered. Like for example both username and password are saved into the store.

After inspecting with Reactotron, the store is actually evolved as expected, both username and password, along with other fields are updated.

Tech stack

React Native app (not Expo), with Redux

package.json

"dependencies": {
    "react": "16.6.1",
    "react-native": "0.57.7",
    "react-native-gesture-handler": "^1.0.12",
    "react-native-i18n": "^2.0.15",
    "react-navigation": "^3.0.8",
    "react-redux": "^6.0.0",
    "redux": "^4.0.1",
    "redux-persist": "^5.10.0"
  },

Application status

I have create a simple ReactNative app where the root component is:

App.js

import React, {Component} from 'react';
import { Provider } from 'react-redux';
import LoadingIndicator from '@components/Visual/LoadingIndicator/';
import MainNavigator from '@components/Visual/MainNavigator'
import {checkUserState} from '@actions/GlobalActions'
import {store, persistor} from '@components/Storage/StorageConfigurationBuilder'
import { PersistGate } from 'redux-persist/integration/react'

export default class App extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    store.dispatch(checkUserState())
  }

  render() {
    return (
        <Provider store={store}>
          <PersistGate loading={<LoadingIndicator />} persistor={persistor}>
              <MainNavigator initialRoute={this.props.initialRoute} />
          </PersistGate>
        </Provider>
    );
  }
}

@components and @actions in the imports are resolved by the babel plugin "module-resolver"

.babelrc

...
["module-resolver", {
      "root": ["."],
      "alias": {
        "^@constants/(.+)": "./constants/\\1",
        "@i18n": "./i18n/index.js",
        "^@components/(.+)": "./components/\\1",
        "^@screens/(.+)": "./screens/\\1",
        "^@reducers/(.+)": "./reducers/\\1",
        "^@actions/(.+)": "./actions/\\1",
      }
    }]
...

the StorageConfigurationBuilder imported in the route app which provides both the store and the persistor is simply:

StorageConfigurationBuilder.js

import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage/index.native'
import AllReducers from '@reducers/AllReducers'
//import Reactotron from '../../ReactronConfig'

const persistConfig = {
    key: 'root',
    storage
}
const persistedReducer = persistReducer(persistConfig, AllReducers)

export const store = createStore(persistedReducer)
export const persistor = persistStore(store)

Finally the view component that is being rendered, which is separated into two parts

index.js

import React, {Component} from 'react'
import { connect } from 'react-redux';
import LoginView from './LoginView';
import {loginFetch, loginFetchError, loginFetchSuccess} from '@actions/UserActions'
import ApiConfiguration, { getApiBaseUri } from '@constants/ApiConfiguration'

class LoginController extends Component {
    constructor(props) {
        super(props)
    }

    doLogin() {
      this.props.dispatch(loginFetch());

      fetch(getApiBaseUri() + ApiConfiguration.endpoints.authentication.path, {
          ...ApiConfiguration.headers,
          method: ApiConfiguration.endpoints.authentication.method,
          body: JSON.stringify({
            email: this.props.email, 
            password: this.props.password
          })
        }).then((response) => {
          return response.json()
        }).then((response) => {
          this.props.dispatch(loginFetchSuccess(response))
        }).catch((e) => {
          this.props.dispatch(loginFetchError(e))
        })
    }

    render() {
        return <LoginView {...this.props} login={this.doLogin.bind(this)} />;
    }
}

const mapStateToProps = (state) => {
    const { user } = state
    return { user }
};

export default connect(mapStateToProps)(LoginController);

The view itself <LoginView /> has nothing special about it

LoginView.js

import React from 'react';
import {StyleSheet, SafeAreaView, View, TextInput, Text, TouchableOpacity} from 'react-native';
import {changeEmail, changePassword} from '@actions/UserActions'
import I18n from '@i18n';
import Colors from '@constants/Colors';

export default props => {
    return (<SafeAreaView style={styles.container}>
        <View style={styles.loginForm}>

            <View style={styles.fieldContainer}>
                <Text style={styles.fieldLabel}>{I18n.t('login.email_label')}</Text>
                <TextInput 
                    value={props.user.email}
                    onChangeText={value => props.dispatch(changeEmail(value))}
                    placeholder={I18n.t('login.email_placeholder')} 
                    style={styles.field} 
                    textContentType={"username"} 
                    returnKeyType={"next"}
                    underlineColorAndroid={"transparent"}></TextInput>
            </View>

            <View style={styles.fieldContainer}>
                <Text style={styles.fieldLabel}>{I18n.t('login.password_label')}</Text>
                <TextInput 
                    value={props.user.password}
                    onChangeText={value => props.dispatch(changePassword(value))}
                    style={styles.field}  
                    placeholder={I18n.t('login.password_placeholder')} 
                    textContentType={"password"} 
                    underlineColorAndroid={"transparent"}
                    secureTextEntry={true}></TextInput>
            </View>

            <View style={styles.panelFooter}>
                <TouchableOpacity onPress={() => props.login()}>
                    <Text>
                        {I18n.t('login.login_button')}
                    </Text>
                </TouchableOpacity>
            </View>
        </View>
    </SafeAreaView>);
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: Colors.backgroundColor,
        justifyContent: 'center'
    },
    loginForm: {
        marginLeft: 20,
        marginRight: 20,
        justifyContent: 'space-around',
        borderWidth: 1,
        borderColor: Colors.borderColor,
        borderRadius: 5
    },

    panelFooter: {
        flexDirection: 'row',
        justifyContent: 'flex-end',
        backgroundColor: '#e5e5e5',
        height: 44,
        paddingRight: 20,
        paddingTop: 13,
        marginTop: 35
    },

    fieldContainer: {
        height: 50,
        flexDirection: 'column',
        marginLeft: 20,
        marginRight: 20,
        marginTop: 25
    },
    fieldLabel: {
        fontSize: 13,
        fontWeight: "600",
        marginBottom: 4,
        marginLeft: 10
    },
    field: {
        height: 44,
        borderWidth: 1,
        borderColor: Colors.borderColor,
        borderRadius: 22,
        fontSize: 18,
        paddingLeft: 22,
        paddingRight: 10
    }
})

Solution

  • So I added a simple reducer / action and attached it to my app, the same App described above. I got this code from another standalone project I created earlier which was working fine.

    This part, the FriendReducer continued to work fine while my reducers described above were still not working.

    Finally I added the following case in the reducer to restore my application state. For some reason it was not doing it on its own.

    case 'persist/REHYDRATE':
       //rehydrating userStore to app
       newState = { ...action.payload.userStore }
       return newState