React Native Pass properties on navigator pop - react-native

I'm using NavigatorIOS on my react native app. I want to pass some properties when navigating back to previous route.
An example case:
I'm in a form page. After submitting data, I want to go back to the previous route and do something based on the submitted data
How should I do that ?

Could you pass a callback func on the navigator props when you push the new route and call that with the form data before you pop to the previous route?

Code sample showing how to use a callback before pop. This is specifically for Navigator and not NavigatorIOS but similar code can be applied for that as well.
You have Page1 and Page2. You are pushing from Page1 to Page2 and then popping back to Page1. You need to pass a callback function from Page2 which triggers some code in Page1 and only after that you will pop back to Page1.
In Page1 -
_goToPage2: function() {
this.props.navigator.push({
component: Page2,
sceneConfig: Navigator.SceneConfigs.FloatFromBottom,
title: 'hey',
callback: this.callbackFunction,
})
},
callbackFunction: function(args) {
//do something
console.log(args)
},
In Page2 -
_backToPage1: function() {
this.props.route.callback(args);
this.props.navigator.pop();
},
The function "callbackFunction" will be called before "pop". For NavigatorIOS you should do the same callback in "passProps". You can also pass args to this callback. Hope it helps.

You can use AsyncStorage, save some value on child Component and then call navigator.pop():
AsyncStorage.setItem('postsReload','true');
this.props.navigator.pop();
In parent Component you can read it from AsyncStorage:
async componentWillReceiveProps(nextProps) {
const reload = await AsyncStorage.getItem('postsReload');
if (reload && reload=='true')
{
AsyncStorage.setItem('postsReload','false');
//do something
}
}

For NavigatorIOS you can also use replacePreviousAndPop().
Code:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Text,
TouchableOpacity,
View,
AppRegistry,
NavigatorIOS
} = React;
var MainApp = React.createClass({
render: function() {
return (
<NavigatorIOS
style={styles.mainContainer}
initialRoute={{
component: FirstScreen,
title: 'First Screen',
passProps: { text: ' ...' },
}}
/>
);
},
});
var FirstScreen = React.createClass({
render: function() {
return (
<View style={styles.container}>
<Text style={styles.helloText}>
Hello {this.props.text}
</Text>
<TouchableOpacity
style={styles.changeButton} onPress={this.gotoSecondScreen}>
<Text>Click to change</Text>
</TouchableOpacity>
</View>
);
},
gotoSecondScreen: function() {
console.log("button pressed");
this.props.navigator.push({
title: "Second Screen",
component: SecondScreen
});
},
});
var SecondScreen = React.createClass({
render: function() {
return (
<View style={styles.container}>
<Text style={styles.helloText}>
Select a greeting
</Text>
<TouchableOpacity
style={styles.changeButton} onPress={() => this.sayHello("World!")}>
<Text>...World!</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.changeButton} onPress={() => this.sayHello("my Friend!")}>
<Text>...my Friend!</Text>
</TouchableOpacity>
</View>
);
},
sayHello: function(greeting) {
console.log("world button pressed");
this.props.navigator.replacePreviousAndPop({
title: "First Screen",
component: FirstScreen,
passProps: {text: greeting}
});
}
});
var styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: "#eee"
},
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
marginTop: 50,
},
helloText: {
fontSize: 16,
},
changeButton: {
padding: 5,
borderWidth: 1,
borderColor: "blue",
borderRadius: 4,
marginTop: 20
}
});
AppRegistry.registerComponent("TestApp", () => MainApp);
You can find the working example here: https://rnplay.org/apps/JPWaPQ
I hope that helps!

I had the same issue with React Native's navigator which I managed to solve using EventEmitters and Subscribables. This example here was really helpful: https://colinramsay.co.uk/2015/07/04/react-native-eventemitters.html
All I needed to do was update for ES6 and the latest version of React Native.
Top level of the app:
import React, { Component } from 'react';
import {AppRegistry} from 'react-native';
import {MyNavigator} from './components/MyNavigator';
import EventEmitter from 'EventEmitter';
import Subscribable from 'Subscribable';
class MyApp extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
this.eventEmitter = new EventEmitter();
}
render() {
return (<MyNavigator events={this.eventEmitter}/>);
}
}
AppRegistry.registerComponent('MyApp', () => MyApp);
In the _renderScene function of your navigator, make sure you include the "events" prop:
_renderScene(route, navigator) {
var Component = route.component;
return (
<Component {...route.props} navigator={navigator} route={route} events={this.props.events} />
);
}
And here is the code for the FooScreen Component which renders a listview.
(Note that react-mixin was used here in order to subscribe to the event. In most cases mixins should be eschewed in favor of higher order components but I couldn't find a way around it in this case):
import React, { Component } from 'react';
import {
StyleSheet,
View,
ListView,
Text
} from 'react-native';
import {ListItemForFoo} from './ListItemForFoo';
import reactMixin from 'react-mixin'
import Subscribable from 'Subscribable';
export class FooScreen extends Component {
constructor(props) {
super(props);
this._refreshData = this._refreshData.bind(this);
this._renderRow = this._renderRow.bind(this);
var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds.cloneWithRows([])
}
}
componentDidMount(){
//This is the code that listens for a "FooSaved" event.
this.addListenerOn(this.props.events, 'FooSaved', this._refreshData);
this._refreshData();
}
_refreshData(){
this.setState({
dataSource: this.state.dataSource.cloneWithRows(//YOUR DATASOURCE GOES HERE)
})
}
_renderRow(rowData){
return <ListItemForFoo
foo={rowData}
navigator={this.props.navigator} />;
}
render(){
return(
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderRow}
/>
)
}
}
reactMixin(FooScreen.prototype, Subscribable.Mixin);
Finally. We need to actually emit that event after saving a Foo:
In your NewFooForm.js Component you should have a method like this:
_onPressButton(){
//Some code that saves your Foo
this.props.events.emit('FooSaved'); //emit the event
this.props.navigator.pop(); //Pop back to your ListView component
}

This is an old question, but currently React Navigation's documentation for Passing params to a previous screen suggests that we use navigation.navigate() and pass whatever parameters we want the previous screen to have.

Related

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

Save react-native-check-box status after reload

I am building a React Native Iphone App.I have a checkbox "Remember me" in Login page, which I want to set to remember the username and password in order to login.I want to save the status of checkbox even after reload(Once it is ticked it should persist till it is ticked-off by the user).Below is my code.
index.js :
import React, { Component } from 'react';
import { View,KeyboardAvoidingView, Text, StyleSheet, Dimensions} from 'react-
native';
import CheckBox from 'react-native-check-box';
import AsyncStorage from '#react-native-community/async-storage';
export default class index extends Component{
constructor() {
super();
this.state = {
status: false
};
toggleStatus = async() =>{
this.setState({
status: !this.state.status
});
AsyncStorage.setItem("myCheckbox",JSON.stringify(this.state.status));
}
}
componentWillMount(){
AsyncStorage.getItem('myCheckbox').then((value) => {
this.setState({
status: (value === 'true')
});
});
}
render() {
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: 'white', justifyContent: 'flex-end'}}
behavior="padding"
keyboardVerticalOffset={50}
enabled>
<Text>{typeof this.state.status +' : '+ this.state.status}</Text>
<CheckBox
style={{flex: 1,paddingLeft:100,paddingTop:20}}
onClick={()=>{
this.setState({
isChecked:!this.state.isChecked
})
toggleStatus(this)
}}
isChecked={this.state.isChecked}
rightText={"Remember me"}
/>
</KeyboardAvoidingView>
);
}
}
index.navigationOptions = {
headerTitle: ''
};
const styles = StyleSheet.create({
});
I could save the status but not set it after reload.I have tried some techniques using the stackoverflow logics, but dint give me proper result.Can anyone help me to set the checkbox.Thanks in advance.
I think you are making a mistake in your toggle method. async doesn't work here (Also we need to use await with async) you should write your code like this. setState take time to save the state so you need to use its callback function which called after the state saved.
I am showing 2 ways here but I prefer the first one.
toggleStatus =() =>{
this.setState({
status: !this.state.status
}, () => AsyncStorage.setItem("myCheckbox",JSON.stringify(this.state.status)));
}
OR
You can do like
toggleStatus = () =>{
AsyncStorage.setItem("myCheckbox",JSON.stringify(!this.state.status));
this.setState({
status: !this.state.status
});
}

how to pass form data from screen1 to screen2 in react native?

how to pass form data from screen1 to screen2 in react native ? I have following code in scrren1 I want posted amount data in screen2. Please let me know how can I pass data on screen2 and receive it in react native?
import React, { Component } from 'react';
import { Button, View, Text, StyleSheet } from 'react-native';
import t from 'tcomb-form-native'; // 0.6.9
const Form = t.form.Form;
const User = t.struct({
amount: t.String,
});
const formStyles = {
...Form.stylesheet,
formGroup: {
normal: {
marginBottom: 10
},
},
controlLabel: {
normal: {
color: 'blue',
fontSize: 18,
marginBottom: 7,
fontWeight: '600'
},
// the style applied when a validation error occours
error: {
color: 'red',
fontSize: 18,
marginBottom: 7,
fontWeight: '600'
}
}
}
const options = {
fields: {
amount: {
label: "Enter Amount You want to Top up",
error: 'Please add amount to proceed ahead!'
},
},
stylesheet: formStyles,
};
class HomeScreen extends Component {
static navigationOptions = {
title: 'Home',
};
handleSubmit = () => {
const value = this._form.getValue();
console.log('value: ', value);
}
render() {
return (
<View style={styles.container}>
<Form
ref={c => this._form = c}
type={User}
options={options}
/>
<Button
title="Pay Now"
onPress={this.handleSubmit}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
});
export default HomeScreen;
It depends if you want to pass data between Parent to Child, Child to Parent or Between Siblingsā€Š
I suggest you to read Passing Data Between React Components, old but this article did help me to understand the logic behind passing data as it's not as easy to implement as in other programming languages.
Excerpt using props:
class App extends React.Component {
render() {
[... somewhere in here I define a variable listName
which I think will be useful as data in my ToDoList component...]
return (
<div>
<InputBar/>
<ToDoList listNameFromParent={listName}/>
</div>
);
}
}
Now in the ToDoList component, use this.props.listNameFromParent to access that data.
You have many ways to send informations from one screen to another in React Native.
eg.
Use React Navigation to navigate between your scenes. You will be able to pass params to your components, which will be accessible in the navigation props when received.
this.props.navigation.navigate({routeName:'sceneOne', params:{name} });
You can also send directly props to a component, and treat them in it. In your render section of your first component, you could have something like this :
<myComponent oneProps={name}/>
In that example, you will receive the props "oneProps" in your second component and you will be able to access it that way :
type Props = {
oneProps: string,
}
class myComponent extends React.Component<Props> {
render() {
console.log('received sent props', oneProps);
return (
<View> // display it
<Text>{this.props.oneProps}</Text>
</View>
);
};
}
These are only two effective solutions, but there are a lot more.
Hope it helped you :)
Have a good day

Navigate to another page after setState gives error the 2nd time

I'm using react-native-navigation (not sure if that would be relevant) and trying to handle simple props. However, when I go to ScreenB from ScreenA, it works without any error. After that, if I pop() screen to ScreenA and then click again on the same component to go to ScreenB, the app crashes with the following error:
You attempted to set the key a with the value 2 on an
object that is meant to be immutable and has been frozen.
Here's a minimal example of the code in ScreenA: Preview:
import React, { Component } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { navigationActions } from 'react-native-navigation';
export default class Preview extends Component {
constructor(props) {
super(props);
this.state = {
data: { a: 0, b: 1, c: 2 }
};
}
_goToFullview() {
let temp = this.state.data;
temp.a = 2;
this.setState({ data: temp });
this.props.navigator.push({ screen: "App.FullView", passProps: { data: this.state.data } });
}
render() {
return (
<View>
<Text>{"a: " + this.state.data.a + ","}</Text>
<Text>{"b: " + this.state.data.b + ","}</Text>
<Text>{"c: " + this.state.data.c}</Text>
<TouchableOpacity onPress={ () => this._goToFullview() }>
<Text>Go To Fullview</Text>
</TouchableOpacity>
</View>
)
}
}
And here is ScreenB: FullView:
import React, { Component } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { navigationActions } from 'react-native-navigation';
class Fullview extends Component {
render() {
return (
<View>
<Text>{"a: " + this.props.data.a + ","}</Text>
<Text>{"b: " + this.props.data.b + ","}</Text>
<Text>{"c: " + this.props.data.c}</Text>
<TouchableOpacity onPress={ () => this.props.navigator.pop({ animated: true, animationType: 'fade' }) }>
<Text>Go back to preview</Text>
</TouchableOpacity>
</View>
)
}
}
I believe the above information should be sufficient to understand my problem. Can anyone explain why this would be happening and a simple solution or hint to fix this?
There are some issues on react-native-navigation which are related to yours.
Somehow, passing some props to the navigator will make them immutable and frozen.
So in your Screen A when you do
this.props.navigator.push({ screen: "App.FullView", passProps: { data: this.state.data } });
It is possible that this.state.data become immutable and when you try to modify it the next time it breaks.
Can you try by passing another reference like this :
this.props.navigator.push({ screen: "App.FullView", passProps: { data: {...this.state.data} } });
To see if it works ?
EDIT :
If yours data.a, data.b and data.c are also object you will maybe need to make a deepClone instead (see : https://github.com/wix/react-native-navigation/issues/394#issuecomment-255936630)
this.props.navigator.push({ screen: "App.FullView", passProps: JSON.parse(JSON.stringify(this.state.data) } });

Redux & NavigatorIOS does not update state in child views in react-native?

How can I make redux update master and detail views contained inside a NavigatorIOS?
Both the Master and Detail views do not update:
MainView > TabBar > TabBarItem > NavigatorIOS > MasterMenu > Detail
Other TabBarItem's work great using redux. For example:
MainView > TabBar TabBarItem > ViewProfile
<Icon.TabBarItem
style={{borderColor:'red', borderWidth:0}}
{...this.props}
iconName="timeline"
title="View"
selected={this.state.selectedTab === 'viewTab'}
onPress={() => {
this.setState({
selectedTab: 'viewTab',
presses: this.state.presses + 1
});
}}>
<ViewProfile {...this.props} />
</Icon.TabBarItem>
This MainView > TabBar > TabBarItem > NavigatorIOS > MasterMenu > Detail does NOT update:
<Icon.TabBarItem
style={{borderColor:'red', borderWidth:0}}
title={'Menu'}
iconName="more-vert"
selected={this.state.selectedTab === 'moreTab'}
onPress={() => {
this.setState({
selectedTab: 'moreTab',
});
}}>
<View style={ss.container}>
<NavigatorIOS
ref="nav"
style={ss.container}
{...this.props}
initialRoute={{
title: 'Master Menu',
component: MasterMenu,
passProps: {
...this.props,
},
}}
/>
</Icon.TabBarItem>
</TabBarIOS>
This is how I connect and pass props to MainView that contains the NaigatorIOS > MasterMenu > Detail
File: mainApp.js
class MainApp extends Component {
constructor(props) {
super(props);
}
render() {
console.log('****** MainApp::render ****');
const { state, actions } = this.props;
return (
<MainView
{...this.props} />
);
}
}
// mapStateToProps
function mapStoreStateToComponentProps(state) {
return {
state: state.appState,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, appActions), dispatch),
}
}
export default connect(
mapStoreStateToComponentProps,
mapDispatchToProps
)(MainApp)
I can drill down into a NavigatorIOS containing a MasterMenu->DetailMenu view, call an action in the detail view, the state changes and all other views in TabBarIOS update. However, the redux state in both the master and detail views contained inside the <NavigatorIOS> retain their original values.
Is passProps not the way to do it?
Or is there other listen or subscribe methods that a master and child view can use to update when data store state changes?
Known issue with NavigatorIOS
NavigatorIOS does not rerender a scene, when passProps changed.
Bypass by connecting redux at a lower point in the hierarchy
You could bypass this issue by connecting your component MasterMenu with redux, instead of MainView.
The advice of #purii solved my problem.
I created a wrapper for the "Master View" component loaded into NavigatorIOS component.
I also created wrappers for all "Child View" components that were drilled-down into from the "Master View" component. This seems ugly and wrong but it works. Using NavigatorIOS passProps seemed to break redux updates.
var React = require('react-native');
var {
Component,
} = React;
import {bindActionCreators} from 'redux';
import { connect } from 'react-redux';
import * as appActions from '../actions/appActions';
var MasterListView = require('../../MasterListView');
class MasterListWrapper extends Component {
constructor(props) {
super(props);
}
render() {
console.log('****** PlanList::render ****');
return (
<MasterListView {...this.props} />
);
}
}
// mapStoreStateToComponentProps would be a more descriptive name for mapStateToProps
function mapStateToProps(state) {
return {
state: state.appState,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, appActions), dispatch),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(MasterListWrapper)
And then I pass MasterListWrapper as the NavigatorIOS component:
<Icon.TabBarItem
style={{borderColor:'red', borderWidth:0}}
title="Plan"
iconName="list"
selected={this.state.selectedTab === 'planTab'}
onPress={() => {
this.setState({
selectedTab: 'planTab',
presses: this.state.presses + 1
});
}}>
<View style={ss.container}>
<NavigatorIOS
ref="nav"
style={ss.container}
initialRoute={{
title: 'Field Planner',
component: MasterListWrapper,
passProps: {
},
}}
/>
</View>
</Icon.TabBarItem>
In the MasterListView I needed to add componentWillReceiveProps to get my ListView to re-render when redux state changed.
componentWillReceiveProps: function(nextProps) {
this.setState({
// Force all rows to render
dataSource: ds.cloneWithRows(nextProps.state.waypointItemList),
});
},
This is likely not the best approach. I will be looking for better approaches.
However at this point in my learning curve of javascript and react-native, I am happy it worked.
Redux is pretty cool. I have multiple tabs. I can go to menu and change a settings, for example, change units from feet to meters, and all my views are re-rendered.