Is navigation.goBack(); navigation.navigate(...); valid? - react-native

What's the recommended way to eliminate a screen from the stack? I have a few cases where a user submits info on one screen (creating an account, or conducting a transaction, etc) and I'd like the input screen to be removed such that they're routed to the result screen and going back takes them to the screen prior to entering the info.
The ideal flow would be something like Item Screen -> Purchase Screen -> Result Screen --(goBack)--> Item Screen, to prevent confusion or double submission of collected info.
What I'm doing currently is navigation.goBack(); navigation.navigate('ResultScreen');, but I'm getting warnings about setting state on an unmounted component (the message suggests this is a memory leak). I don't see any obvious setState calls in my code on that path, so I'm thinking that either navigation.navigate() is doing a setState/forceUpdate (forceRender? whatever..) under the covers or that I'm otherwise not doing this in the intended way.
Searching the docs/stackoverflow/github issues hasn't been fruitful for guidance on this. Any suggestions?
Thanks!

The best way to achieve this so you don't over pollute your stack is by using the Reset action.
import { StackActions, NavigationActions } from 'react-navigation';
const resetAction = StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'ItemScreen' })
],
});
this.props.navigation.dispatch(resetAction);
This way you are resetting the stack and eliminating the possibility of someone re-navigating back through your various items.

Related

React native - State won't update on navigation

I'm not sure if the title is informative enough, but I'll try explaining my need.
I have two stacks in my application. I have a floating component that appears everywhere in my app(No matter in which stack I am), that shows some items, and by clicking them it will navigate to a component that renders this item. The navigation code is:
navigation.navigate('Tabs', {
screen: 'Home',
params: { screen: 'Dish', params: { from: '', data: dish } },
})
Now the problem is, if I'm already inside the screen Dish that render this item, and then use the floating component to navigate to another item, my state isn't rerendering, and keeps its old values.
I've managed to solve this by changing the code to:
navigation.push('Dish', {from: '', data: dish})}
Which simply push the component into the stack, though it made another problem; If I'm in my second stack (not the Tab one), then this doesn't work and won't navigate me anywhere, which make sense..
I also managed to solve this by navigating to the Tab stack and then pushing my component like this:
navigation.navigate('Tabs', {screen:'Home'})
navigation.push('Dish', {from: '', data: dish})}
This works, though I'm not sure if this is good practice. I was wondering if this is the correct way of achieving what I want.. Maybe I should just make my component rerender so the state changes? I tried to include as little code as I could, if anything else is needed I'll add it..
Thanks in advance!
It's not the best solution but you can use this :
componentDidUpdate(prevProps) {
if (this.props.route.params !== prevProps.route.params) {
//What you want to do
}
Using the DidUpdate only when the params change to avoid infinite loop.
Or looking after listener.

React life cicle and AsyncStorage

I'm creating an app which passes some crucial info via AsyncStorage, but now have a problem when updating it on another screen.... Let's see:
On Screen 1 :
Load data from AsyncStorage on componentDidMount
componentDidMount() {
AsyncStorage.getItem("userChats").then((value) => {
this.setState({userChats: JSON.parse(value)});
});
}
then on Screen 2 I modify userChats.....
I'll like that when coming back again to Screen 1, the changes made on Screen 2 be reflected on Screen 1 but there are NOT as componentDidMount is not trigged again...........
What's the correct way to do it?
Thanks
componentDidMount is a life-cycle method. Navigating from Screen1 to Screen2 does NOT unmount Screen1. So, when you come back from Screen 2 to Screen 1, the Screen 1 does not mounting because it was NOT unmounted. Hence, componentDidMount is not called.
Whats's the correct way of doing this?
You should use Context API. When you load from AsyncStorage, set that value to Context as well. When you update the value, write changes to both AsyncStorage and Context.
PS: The AsyncStorage may not needed. It depends on your requirement. Most probably, you will be able to achieve this only with Context API.
Please check the following snack. It is done using hooks. You can do the same using class components.
https://snack.expo.io/3L9QSqWqt
UPDATE:
If the data to be handled is too large, it is not recommended to use Context since it saves all the data in the device RAM and consuming too much RAM may result in app crash.
To do this without using context:
(1) Define a function to retrieve data from AsyncStorage.
loadData() {
AsyncStorage.getItem("userChats").then((value) => {
this.setState({userChats: JSON.parse(value)});
});
}
(2) Call it in componentDidMount.
componentDidMount() {
this.loadData()
}
(3) When navigating to the Screen2, pass a callback function as a prop to call loadData function.
this.props.navigation.navigate('Screen2', {
onGoBack: () => this.loadData(),
});
(4) Then in the Screen2, before goBack, you can do this:
await AsyncStorage.setItem('userChats', updatedData);
this.props.navigation.state.params.onGoBack();
this.props.navigation.goBack();
Then, the loadData function is called in the screen2.
PS: Since you use state to store the retrieved data from the AsyncStorage, you can also directly load them into the Context and use. But, keep in mind that, using too much of RAM may cause app crash.

track UI elements states with one object, but the states are not reserved once leaving the screen and coming back

In my react-native project, I have three checkboxes, I need to track the state of those checkboxes so I use an object with key-value (value is boolean) to represent the states of all three checkboxes and use useState hook to manage them. Here is my code:
import React, { useState, useEffect } from 'react';
...
const MyScreen = ({ navigation }) => {
// initially, all checkboxes are checked
const initialCheckBoxState = {
0: true,
1: true,
2: true,
};
const [checkBoxesState, setCheckBoxesState] = useState(initialCheckBoxState);
useEffect(() => {
return () => {
console.log('Screen did unmount');
};
}, [checkBoxesState]);
return (
<View>
...
<SectionList
sections={options}
renderItem={({ index, item }) => (
<CheckBox
onPress={() => {
const checkBoxesStateCopy = { ...checkBoxesState };
checkBoxesStateCopy[index] = !checkBoxesStateCopy[index];
setCheckBoxesState(checkBoxesStateCopy);
}}
/>
)}
/>
...
</View>
);
};
I omitted code that is not the concern of my problem. As you can see, for each item I draw one CheckBox component.
In practice, there are always three items (i.e. three check boxes to show). At the beginning I declared initialCheckBoxState, each key-pair represents the state of the checkbox of each. In the onPress callback of Checkbox I toggle each check box state & update the checkBoxesState by hook method setCheckBoxesState as a whole.
Everything works fine at runtime, my screen is re-rendered when toggling checkbox state, UI shows the status of checkboxes correctly. But issue comes when I navigate back to the previous screen and navigate back to this screen, all checkboxes states are back to the initial states.
So, why the checkboxes states are not reserved?
P.S. previous screen and MyScreen are under the same stack navigator. User press a button of previous screen to navigate to MyScreen. From MyScreen user can go to previous screen by pressing the "headerLeft" button
First lets answer the question:
why the checkboxes states are not reserved?
This component is handling its state completely independent, the state is created & handled inside and no values are passed-in from outside. what does it mean? this component has its initial state value inside of itself, it doesn't use any prop or anything else to initialize the state. everytime this component gets created, state is again initialized with that value. so that's the reason you lose all changes done to checkboxes, because when you leave this screen(component) , it gets unmounted(we'll talk about this in next question) and because all values are just handled inside, every data (containing checkboxes state) will be lost.
So now lets talk about this:
is react-native supposed to reserve the state when come back to the screen?
short answer is No. Every component is destroyed when unmounted including their state and data.
Now lets answer why
screens are still on the stack in memory, not destroyed?
Usually developers use a package like react-navigation or RNRF(which is built on top of react-navigation) for react navigation, most of times we don't care about how they handle this navigation logic, we just use the interface the provided us. each of these packages may have their own way to handle navigation. providing full answer to determine why exactly the screen in still in memory needs full code review and sure lots of debugging but i guess there are 2 possibilities. first as i said maybe the package you are using keeps the unmounted screens in memory at least for a while for some reason. the 2nd is a common react community issue which is Unmounted component still in memory which you can check at: https://github.com/facebook/react/issues/16138
And at last lets answer the question:
how do i keep checkboxes state even with navigating back and losing component containing their state?
This doesn't have just one way to that but simple and short answer is move your state out of the that component, e.g move it out to the parent component or a global variable.
to make it more clear lets explain like this: imagine screen A is always mounted, then you go in B and there you can see some checkboxes and you can modify the states. if the state is handled completely inside B, if you navigate back from screen B to A you lose all changes because B is now unmounted. so what you should do it to put checkboxes states in A screen then pass the values down to B. and when modifying the values, you modify A state. so when B gets unmounted all changes are persistant because you have them in A.
other approached exists as well, you can create a global singleton object named globalState. then put values needed to share between multiple screens there. if you prefer redux or mobx you can use them. one of their usages is when you have some data that you need to share between mutiple screens, these data are independent from where you are at and will persist.
This explanation is from official react-navigation documentation:
Consider a stack navigator with screens A and B. After navigating to
A, its componentDidMount is called. When pushing B, its
componentDidMount is also called, but A remains mounted on the stack
and its componentWillUnmount is therefore not called.
When going back from B to A, componentWillUnmount of B is called, but
componentDidMount of A is not because A remained mounted the whole
time.
https://reactnavigation.org/docs/navigation-lifecycle/#example-scenario
Your MyScreen screen is equivalent to screen B from the example, which means you can expect your screen to stay mounted if you navigate forward, but not backwards.
Its simple, just add a keyExtractor to your SectionList component, which would uniquely identify each checkbox, so that react knows which one to re-render on update.
You'll want to use AsyncStorage to persist data to the device. State variables will be cleared any time the component unmounts.
AsyncStorage docs:
https://react-native-community.github.io/asaync-storage/
import AsyncStorage from '#react-native-community/async-storage';
//You can only store string values so convert objects to strings:
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value)
await AsyncStorage.setItem('#storage_Key', jsonValue)
} catch (e) {
// saving error
}
}
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('#storage_Key')
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch(e) {
// error reading value
}
}
UPDATE -
State is not being persisted due to the nature of React Component lifecycles. Specifically, when you navigate away from a screen the lifecycle method componentWillUnmount is called.
Here's an excerpt from the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().
...Once a component instance is unmounted, it will never be mounted again.
This means any values stored in state will be destroyed as well and upon navigating back to the screen ComponentDidMount will be called which is where you may want to assign persisted values back to state.
Two possible approaches aside from AsyncStorage that may work for some use cases to persist data across screens is using Context or a singleton.

Reset navigation without redux

Currently, as far as I know reset in react-navigation is done by dispatching a redux action of NavigationActions.reset();
Console logging this.props.navigation shows a reset function, but if I try to use it as this.props.navigation.reset('Home'), it throws me an error newStackActions.map is not a function.
Can we reset the whole navigation without using redux? There should be such functionality, I guess I can't find it :/
You can make use StackActions to reset a particular stack, and dispatch it using navigation's internal dispatcher
import { StackActions, NavigationActions } from 'react-navigation';
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Profile' })],
});
this.props.navigation.dispatch(resetAction);
So apparently, after trying out some methods. replace seems to work just fine.
this.props.navigation.replace(ScreenName) replaces your current screen to the specified screen without any increment to the screen stack, essentially you just switch screens over.
I'll just leave it here in case anyone needs help.
EDIT
Note - In case anyone is confused, replace does not mimic the behaviour of a reset function.
replace just replaces your current active screen or the topmost screen on the stack with the given screen whereas
reset essentially wipes off your whole navigation history and creates a new navigation object based on the screens references you provide. In other words you can essentially create a navigation stack full of screens you never visited. (not sure how it is possible ;))

How to navigate to a different view that is not a child?

I am new to react native and new to iOS (not programming) so please excuse me if this question is a simple one. I am trying to navigate from one view to another (with a transition), however they are not related so I do not need the back navigation. I actually do not have a navigation bar at all. When using the Navigator component it seems to not support this at all. I am not sure if there is a separate way to do this but I am not able to figure it out without implementing my own hack.
If I use the navigator component and keep pushing on the views then it just keeps them all in memory and I do not want that. I can transition from one view to another and then pop but I may end up going to the wrong view in that case. I can also replace the view but it seems that does not allow for transitions.
To give you a scenario think of it like this:
Application starts and loads a "Loading" screen.
When initial loading is complete it will then go to the "Login" screen.
There is a button on the "Login" screen to "Register" or "Retrieve Password".
If they click "Register" it will take them there with a button back to "Login".
If they click "Retrieve Password" it will take them to a page with buttons to go back to "Login" or "Register".
So by this example you can see that there is no way to pop because if you were on the login screen and went to the register screen and then wanted to go the retrieve password screen then pop just simply wouldn't work. I do not want any navigation controls on the screen I just want to be able to do a smooth transition between these screens.
Now I was able to find a way to do this but I had to add a method to the Navigator class and hack code in using some of there core methods which seems like its not a good idea at all but here is the code (note this is really just a hack to see if it would work):
Navigator.prototype.pushWithUnmount = function(route) {
var activeLength = this.state.presentedIndex + 1;
var activeStack = this.state.routeStack.slice(0, activeLength);
var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength);
var nextStack = activeStack.concat([route]);
var destIndex = nextStack.length - 1;
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
this.props.configureScene(route),
]);
this._emitWillFocus(nextStack[destIndex]);
this.setState({
routeStack: nextStack,
sceneConfigStack: nextAnimationConfigStack,
}, () => {
this._enableScene(destIndex);
this._transitionTo(
destIndex,
null, // default velocity
null, // no spring jumping
() => {
this.replaceAtIndex(nextStack[destIndex], 0);
this.setState({
presentedIndex: 0,
});
}
);
});
}
By using the code provided above I am now able to do:
this.props.navigator.pushWithUnmount({ component: SomeComponent });
With this code the views are pushed onto the stack with a transition and the old views are unmounted when its finished.
Please tell me that I am doing something wrong and that there is a better way to do this?
The default router with React Native is pretty limited. I'd check out React Native Router Flux. We just switched to it a few weeks ago in our product and have really liked it. It does exactly what you want.