React Native Navigation and Redux Persist - react-native

I am trying to integrate redux-persist with wix react-native-navigation. However, I am unable to find any examples or documentation stating the boilerplate code needed to integrate the both libraries.
I was wondering if anyone would like to share their solution if they have solved this issue ?

First of all, the basic setup should be the similar with or without react-native-navigation as described in the documentation in store.js:
import { persistStore, persistCombineReducers } from 'redux-persist'
import storage from 'redux-persist/es/storage' // default:
localStorage if web, AsyncStorage if react-native
import reducers from './reducers' // where reducers is an object of
reducers
const config = {
key: 'root',
storage,
}
const reducer = persistCombineReducers(config, reducers)
function configureStore () {
// ...
let store = createStore(reducer)
return store
// We'll skip persistStore for now
// let persistor = persistStore(store)
//return { persistor, store }
}
The persistStore call is commented out as we'll do it below. The persistStore method takes a callback in its third argument. The callback is executed after the state is restored/rehydrated. This is nice because this means we can delay starting the screen(s) until the state is rehydrated.
Let's assume you have the following bootstrap code in App.js:
store = configureStore()
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
Now we can add persistStore and wrap your bootstrap code in it like this:
store = configureStore()
persistStore(store, null, () => {
registerScreens(store, Provider)
Navigation.startTabBasedApp({
tabs: [{...},]
})
})
Note:
In v4, you pass config instead of null: persistStore(store, config, callback)

In case you're looking to integrate it with react-native-navigation v2, in App.js, make sure you call persistStore() inside the registerAppLaunchedListener() :
import { persistStore } from 'redux-persist';
...
Navigation.events().registerAppLaunchedListener(() => {
persistStore(store, null, () => {
Navigation.registerComponentWithRedux(...);
...
Navigation.setRoot({...})
...
})
})

Adding to his solution you can also use subscribe() to check if your user is still logged in. That way they don't need to sign in again if they completely close the app (for those users with a login system) and since it is only called once the store is persisted, you can start your app after this is checked.
import {Platform, AsyncStorage, AppState} from "react-native"
import {Navigation} from "react-native-navigation"
import {registerScreens} from "./routes"
import {Provider} from "react-redux"
import configureStore from "./stores/reduxStore"
import {Component} from "react"
const storage = configureStore()
registerScreens(Provider, storage.store)
let startapp = screen => {
Navigation.startSingleScreenApp({
screen: {
screen, // unique ID registered with Navigation.registerScreen
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarColor: "white",
statusBarTextColorScheme: "dark"
}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {} // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
},
drawer: {
left: {
screen: "Drawer", // unique ID registered with Navigation.registerScreen
passProps: {} // simple serializable object that will pass as props to all top screens (optional)
}
},
tabsStyle: {
// optional, add this if you want to style the tab bar beyond the defaults
tabBarButtonColor: "#ffff00", // optional, change the color of the tab icons and text (also unselected). On Android, add this to appStyle
tabBarSelectedButtonColor: "#ff9900", // optional, change the color of the selected tab icon and text (only selected). On Android, add this to appStyle
tabBarBackgroundColor: "#551A8B", // optional, change the background color of the tab bar
initialTabIndex: 1 // optional, the default selected bottom tab. Default: 0. On Android, add this to appStyle
},
appStyle: {
orientation: "portrait"
}
})
}
storage.persistor.subscribe(() => {
storage.store.getState().user.logged
? startapp("mainscreen")
: startapp("loginscreen")
})

We actually dont need redux-persist. We can make our own redux-persist with:
redux + store.subscribe(handlechange)
handleChange function will run when ever something changes in our store.
Also Using aync-await(promise) we are not blocking the main execution thread.
So Inside create store add something like:
store.subscribe(async ()=>{
try {
await AsyncStorage.setItem("store", JSON.stringify(store.getState()));
} catch (error) {
// Error
}
})
Then inside App.js(first component to load). use AsyncStorage.getItem('store'). Then update the store before app starts.
localstorage on the web is a synchronous function which blocks the main thread.
AsynsStorage in react-native doesn't blocks the main thread.

Related

Dispatching Redux Data and Get the State

I stuck on Redux Implementation during developing an app using React Native and Redux. I do this for the first time and followed this example.
I've already installed Redux and React Native Navigation. I would like to save the state containing data for countries (the user picked a country and would like to keep the choice by the time when it browses to all screens).
Good. I've created a component that could be seen to all screens like this:
LinksScreen.navigationOptions = {
headerTitle: 'Links',
headerRight: <CountriesPickButton/>,
};
Next, I visualize the button and wait for a change in the component. By default, it should show primary country. Next, the user clicks on the button and it opens a modal where has a dropdown menu. For example, I show you the default fetching a country:
import { connect } from 'react-redux'
import store from '../../redux/countries'
export default class CountriesPick extends Component {
render() {.... // here is the button and modal, etc. It's work.
}
constructor(props, context) {
super(props, context);
this.state = store.getState();
store.subscribe(() => {
this.setState(store.getState());
});
this.defaultCountry(251);
}
async defaultCountry(countryId) {
return fetch(URL)
.then((response) => response.json())
.then((responseJson) => {
for (const key of Object.keys(responseJson.result)) {
// this works for current screen: this.setState({ defaultCountry: responseJson.result[key], selectedCountry: responseJson.result[key].country_id });
store.dispatch({ defaultCountry: responseJson.result[key], selectedCountry: responseJson.result[key].country_id , type: 'countries' });
}
return responseJson.result;
})
.catch((error) => {
console.error(error);
});
state = {
showModal: false,
countries: [],
selectedCountry: 0,
defaultCountry: [],
type: 'countries'
};
}
Without store.dispatch({}) I can change the state with the country but it has not to share between screens. That's because I started with Redux.
Here is the Redux code ():
import { createStore } from 'redux'
const defaultState = {
showModal: false,
countries: [],
selectedCountry: 0,
defaultCountry: [],
type: 'countries'
};
function store(state = defaultState) {
return state;
}
export default createStore(store);
Something is not like it should be. When I invoke store.dispatch({...}) it's not changing the state, it returns the default array. I guess I should use <Provider></Provider> in App.js to catch every change but first, I need to understand what I wrong?
Is it connected at all? In the example that I followed, I did not see connect(). Also, I'm not sure I'm using type properly.
Thank you in advance.
Problems here are the following:
Example on the link you provided is bad to say the least. Do not follow it
You said to be using react-native-navigation, but the code you provided comes from react-navigation. I suggest using the latter, especially for starters
Your createStore code is not going to work, as reducer for the store should be a function of state and action
With that being said, you should definitely see Basic Tutorial of redux with examples. You will almost never have to do store.getState() or store.dispatch() while using react with redux, as react-reduxpackage (included in the tutorial I linked) will do this for you. You will instead declare dependency between your store state and props your component receives

How to navigate to a specific screen and disallow going back with react-native-navigation

I am using react-native-navigation and I have a stack of screens.
When I go from screen A to screen B, I don't want to give the user the option to go back to screen A, just forward.
I am trying Navigation.popTo("screen.B.id") but I'm getting this error:
Is there any way to achieve this? Thanks in advance.
React-navigation
You can reset the stack like this:
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'screenB' })],
});
this.props.navigation.dispatch(resetAction);
React-native-navigation
A workaround would be to catch the back listener like so:
import {BackHandler} from 'react-native';
export default class RoomType extends Component {
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus',payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
onBackButtonPressAndroid = () => {
//code when you press the back button
};
You could try with setting Screen B to your new root.
setStackRoot(componentId, params)
Maybe you have to popToRoot(componentId, mergeOptions?) if necessary.
Source: react-native-navigation docs
In react-native-navigation, there are 2 options you could choose from to achieve what I believe you are looking for.
try adding the below to your topBar option specifically in the child component you are choosing.
backButton: {
visible: false
}
for a small example, the child you don't want a back option for put:
component: {
id: 'screenB',
name: 'screenB',
options: {
title: {
text: 'screenB'
},
topBar: {
// the other options you want
backButton: {
visible: false
}
}
}
}
you can completely reset the root navigation to the new screen.
In my Opinion,
Option 1. is a simple way to flat out just remove the back button from a specific screen to disable the ability to ever go back to the original screen.
Option 2. is nice when you want to remove the previous screen from the entire equation of the app itself.
my personal use-case for option 2:
I made an app that originally opens to a login/register stack. Once logged in/ registered, I save that information to the AsyncStorage and completely reset the root to the home page.
When opening the app for the second time, it checks for user info from the AsyncStorage. If the app finds user information, It sets the root for the home page and rest of the app. If the app doesn't find user information, it sets the root to the login/register stack and the cycle continues.
I hope this helps!
Use createSwitchNavigator from 'react-navigation' for both screens,which will not let back button of second screen to switch to first screen and also header will not be there with back arrow.
In your App.js,
import * as React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { createSwitchNavigator,createAppContainer } from 'react-navigation';
import ScreenOne from './components/ScreenOne ';
import ScreenTwo from './components/ScreenTwo ';
const App=createSwitchNavigator({
ScreenOne :{screen:ScreenOne },
ScreenTwo :{screen:ScreenTwo }
});
export default createAppContainer(App);

How to set config show/hide refresh button on AppBar

Click to see image
Button refresh on AppBar is not refresh on page Dashboard because I just use Component Card but work on page using component List or Datagrid, so I want to config show/hide refresh button on AppBar or how to fix it work for page not use component List or Datagrid.
Sorry I'm not strong in English.
You'll have to fetch some data from the react-admin state for it to work. Indeed, the refresh button just trigger the refreshView action which update the state.admin.ui.viewVersion key of the the react-admin redux state. This key is a simple counter. Internally, we use this counter to check whether we must update some components data. Here is a simple example of a connected Dashboard which can do things when refreshed:
import React, { Component } from "react";
import { connect } from "react-redux";
class Dashboard extends Component {
componentDidMount() {
this.doOnMountAndWhenRefreshed();
}
componentDidUpdate(prevProps) {
if (prevProps.views !== this.props.views) {
this.doOnMountAndWhenRefreshed();
}
}
doOnMountAndWhenRefreshed = () => {
// This is where you do update your component:
// - Make API requests
// - Fetch data from the react-admin store, etc.
};
render() {
const { views } = this.props;
return <div>Refreshed {views} times.</div>;
}
}
const mapStateToProps = state => ({ views: state.admin.ui.viewVersion });
export default connect(
mapStateToProps,
{}
)(Dashboard);
You can see it working in this codesandbox
Edit for newer version of react-admin
import { useVersion } from 'react-admin';
const Dashboard = () => {
const version = useVersion();
return <div>Refreshed {version} times.</div>;
}
In react-admin 4.x I managed to get the desired behaviour like this:
import React from 'react'
import { useQuery } from 'react-query'
const noop = async () => new Date().valueOf()
export const MyDashboard = () => {
const { data } = useQuery('myDashboard', noop)
return (
<div>Last refreshed at {data}</div>
)
}
export default MyDashboard
Note how data represents the value returned by noop().
That way, whenever the user presses the refresh icon in the AppBar, the component is re-rendered.

React-Navigation: navigate from actions file

I'm new to RN and JS.
I want to navigate once a login action is complete, but I cannot get it to work. I am using firebase.
This is from my actions file. It throws a firebase error:
export const LOGIN_USER_SUCCESS = 'login_user_success';
export const loginUserSuccess = (dispatch, user) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user
});
this.props.navigation.navigate('groupMain'); // this doesnt work
};
I also tried putting it into the Login component - this might be a step in right direction because there is no firebase error, but nothing happens. So it seems that all works, except the user is not navigated to the proper screen. (I removed the 'this doesnt work' line from above)
componentWillReceiveProps(nextProps) {
this.onAuthComplete(nextProps);
}
onAuthComplete(props) {
if (props.user) {
this.props.navigation.navigate('groupMain');
}
}
Later, I also found this in the official docs and tried to implement by using this code in my action, but it threw a firebase error:
"dispatch - Send an action to the router
Use dispatch to send any navigation action to the router. The other navigation functions use dispatch behind the scenes.
Note that if you want to dispatch react-navigation actions you should use the action creators provided in this library.
See Navigation Actions Docs for a full list of available actions.
import { NavigationActions } from 'react-navigation'
const navigateAction = NavigationActions.navigate({
routeName: 'Profile',
params: {},
// navigate can have a nested navigate action that will be run inside the child router
action: NavigationActions.navigate({ routeName: 'SubProfileRoute'})
})
this.props.navigation.dispatch(navigateAction)"
thanks!
I solved a similar problem by creating a global service to handle programmatic navigation:
services/navigator.js
import { NavigationActions } from 'react-navigation';
let navigator;
export function setNavigator(nav) {
navigator = nav;
}
export function navigate(routeName, params) {
if (navigator) {
navigator.dispatch(NavigationActions.navigate({routeName, params}));
}
}
export function goBack() { ... }
export function reset() { ... }
Then, in my top-level component I store the reference to the navigator when it's created:
screens/App.js
import { setNavigator } from '../services/navigator';
const AppNavigator = StackNavigator(...);
class App extends Component {
render() {
return (
<AppNavigator ref={nav => setNavigator(nav)} />
);
}
}
And finally in my action files (or wherever I need to), I simply use the service to dispatch navigation actions:
actions/login.js
import { navigate } from '../services/navigator';
export function loginUserSuccess() {
// use navigate() anywhere you'd normally use this.props.navigation.navigate()
navigate('NextScreen');
}

REDUX: understanding a bit the concept + react native

So, I am working on a pretty straight forward mobile app that has these scenes:
a list of people
person profile
add form
now, what I do, when I first load the LIST scene, I make an API call (I have a list component that I populate once I get results from the API... state.people).
All good here... when I tap on a person he's profile opens, no extra API calls, just passing the person object from state.people array.
All good here as well.
When I open ADD NEW person and send the form I make another API call (I post the information and get the new Object back)...
now the bit that is confusing to me.
What I would like is to update the LIST scene state.people by making another API call (get all again) after I get the OK confirmation from the POST.
and then navigate to Person's profile.
but, I am outside the scope of the LIST scene (I am in ADD NEW form). So, what would be the correct redux logic for this one?
The LIST component is already mounted... how do I communicate to LIST if I am on different scene
all these binding actions to components properties is confusing too... why can't redux act like a global hub that would always be accessible and would always retain it's state (at least on mobile app)
There is really a lack of real app examples... so far I see only very simplified examples that are not very useful on the grand scale to understand the whole flow
the store I have
/**
* STORE
*/
'use strict';
import { createStore, applyMiddleware } from 'redux';
import reducer from './_reducer';
import promiseMiddleware from 'redux-promise-middleware';
import thunkMiddleware from 'redux-thunk';
const store = createStore(reducer, {}, applyMiddleware(
thunkMiddleware,
promiseMiddleware()
));
export default store;
and the actions I have:
import * as constants from '../../constants/constants';
import request from '../../utils/request';
export const getAll = () => ({
type: constants.PEOPLE_FETCH,
payload: request(constants.API_PATH + 'person', {method: 'GET'})
});
export const search = (data, searchTerm) => ({
type: constants.PEOPLE_SEARCH,
payload: _filter(data, searchTerm)
});
export const save = (data) => ({
type: constants.PERSON_SAVE,
payload: request(constants.API_PATH + 'person', {method: 'POST', body: JSON.stringify(data)})
});
This can be an example architecture for your app:
Make a Redux store with list of people.
On initial API call, update the store to contain the list fetched by API call.
Wrap your app inside Provider and pass the store to the Provider.
Use connect and mapStateToProps and mapDispatchToProps to connect the Redux store to React state.
Whenever you update or insert new person, and get the new object, you need to dispatch an action which then goes to the reducer function which finally returns the updated Redux store, and dont worry with the re-rendering as React does the re-rendering itself whenever there is a change in a state.
I'll give a small example of store/actions/reducer, with a react + redux app.
store.js
import { applyMiddleware, compose, createStore } from 'redux'
import reducer from './reducer'
import logger from 'redux-logger'
// TOOD: add middleware
let finalCreateStore = compose(
applyMiddleware(logger())
)(createStore)
export default function configureStore (initialState = { todos: [] }) {
return finalCreateStore(reducer, initialState)
}
actions.js
let actions = {
helloWorld: function(data){
return {
type: 'HELLO_WORLD',
data: data
}
}
};
export default actions
reducer.js // Please read from Redux docs that reducers need to be pure functions
export default function myReducer(state = [], action) {
switch (action.type) {
case 'HELLO_WORLD':
return 'welcome' + data;
default:
return state;
}
}
Component.js (the React App) //In component whenever you receive new object, dispatch an action which will modify the store.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import actions from '../redux/actions'
class App extends Component {
handleClick() {
store.dispath(action.helloWorld("jimmy")); //this dispatches an action, which goes to the reducer to change the state and adds 'welcome' before 'jimmy'
}
render() {
return (
<div onClick={this.handleClick.bind(this)}>
{store.getState()} //getState function to access store values
</div>
)
}
}
function mapStateToProps(state) {
return state
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch) //binds all the actions with dispatcher and returns them
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
This works like whenever you click the 'div' in the React Component, it calls the function, handleClick(), in which there is an action dispatch. This action then calls the reducer itself to update the store. I know you might get confused that how is store getting updated. Its a bit confusing but for that you need to follow a basic tutorial to explain React+Redux.
Please note this is not a runnable example, just a pseudocode. I recommend you to watch this youtube series to completely understand the redux stores+ react+webpack