Populate Stack with multiple screens in React Navigation - react-native

After Google for weeks, I'm still yet to find a sensible approach to the following with React Navigation:
I have a Stack with several screens, the app consists of a todo list, of items...
The screens are:
Lists (show all of them)
List (gone into a specific list)
Items (show all items in the list)
Item (adding or editing an item)
When I receive a URL being shared into my app, I want to navigate to the Item screen, but have all the previous screens in place too, for when the user hits back.
This isn't a simple deeplink scenario. The url being shared could be any arbitrary text, not a specific url related to my app. (It's the url someone wants to put on their list).
Is there any way to do this from my App container? I currently try this, but I'm not sure if it's correct/sane.
this.navigationRef.current?.reset({
index: 0,
routes: [ { name: 'Lists' }, { name: 'List' }, { name: 'Items'}, { name: 'Item' } ],
});
This seems to be unmounting screens if they are already mounted, ie, if the user is already on the Item screen.
I found the "initial" param on navigate work well for another case, when I wanted the screen to be the 2nd one in the stack (and the default was used as the initial screen), but there doesn't seem to be a way to go "deeper".
My stack is also one of 3 tabs, if that makes things easier/worse.

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 - Navigating to the same component weird behaviour

I'm breaking my head over this, the behaviour I'm seeing seems weird for me, though it might make sense to some of you.
Consider this component:
const DishPreparation = ({ dish }) => {
const [slideIndex, setSlideIndex] = useState(0)
const sceneRef = useRef(null)
useKeepAwake();
return (
<View style={styles.scene} ref={sceneRef}>
<View flex={0.12} style={{ marginTop:-10 }}>
<ProgressSteps activeStep={slideIndex} activeStepIconBorderColor={colors.lightTan} completedProgressBarColor={colors.lightTan} completedStepIconColor={colors.lightTan}>
{dish.preparationSteps.map((step, index) => (
<ProgressStep removeBtnRow key={index}/> ))}
</ProgressSteps>
......
)
Which is being rendered through another component Dish, that can be navigated to. I have some floating component in my app that allows me to navigate to Dish with a specific dish. If I navigate to a dish through it for the first time, or if I navigate there through other component that doing it by pushing the component to the stack, everything works fine. The weird behaviour is when I'm already inside this component with a specific dish, and then navigate through the floating component to a different dish, it's like the old dish is being kept.
Lets say first dish had 3 elements in dish.preparationSteps, and the second one has 4, then dish.preparationSteps.map(step, index) returns only 3 elements instead of 4, but if I render step.someInfo inside the mapping, then I actually see the new dish values.
How is this happening? I'd expect either 4 elements to be returned, or 3 elements but with the old dish values, how is this mixture happening? Also, dont know if it helps but slideIndex keeps its old value, and doesn't reinitialize to 0.
This is how I navigate through the floating component:
navigation.navigate('Tabs', {
screen: 'Home',
params: { screen: 'Dish', params: { from: '', data: dish } },
})
This is how I navigate to it through other some other component(which works as expected)
navigation.push('Dish', {from: 'DishList', data: item})
If any other code is needed I'll be happy to add it.
When using .navigate instead of .push, navigation will look for that screen name ("Dish") and just change navigation params, without remounting component or opening a new screen with it. Usually you can just use navigation.push (like in your last example), but the problem is that you are trying to navigate from some top-level navigator. You can't use .push there because it will push to the outer navigator which doesn't have "Dish" screen. If you want to add another "Dish" screen on top of the existing one in the nested navigator, you need to navigation.push from the Stack Navigator you want to navigate in; it's only that your floating is not located in that Stack Navigator
First thing you can try is to add a unique key to your screen when navigating, e.g.
navigation.navigate('Tabs', {
screen: 'Home',
params: { screen: 'Dish', key: dish.id, params: { from: '', data: dish } },
})
so that navigation will compare screens not by the name but by the name and key. This will most likely replace existing Dish screen with a new one
Better solution would be to dispatch navigation action that will drill down to the Stack Navigator you want to navigate in, and dispatch StackActions.push there
And even better solution would be not trying to navigate inside inner navigators from outer navigators
Maybe the getId prop is what you're looking for?
From the React Navigation docs:
In a stack navigator, calling navigate with a screen name will result in different behavior based on if the screen is already present or not. If the screen is already present in the stack's history, it'll go back to that screen and remove any screens after that. If the screen is not present, it'll push a new screen.
You could use the getId prop to push a new screen instead. For example, say you have specified a getId prop for Profile screen:
<Screen name={Profile} component={ProfileScreen} getId={({ params }) => params.userId} />
Now, if you have a stack with the history Home > Profile (userId: bob) > Settings and you call navigate(Profile, { userId: 'alice' }) the resulting screens will be Home > Profile (userId: bob) > Settings > Profile (userId: alice) since it'll add a new Profile screen as no matching screen was found.

What is a difference between navigation.navigate(), navigation.push(), navigation.goBack() and navigation.popToTop() if I go back from page to page?

From this and this, I learned that there is a meaningful difference between navigation.navigate() and navigation.push(). Nevertheless, I am still wondering if I can use navigation.navigate() or navigation.push() instead of navigation.goBack() or navigation.popToTop().
In my case, there is one page, and some parameters are included in the navigation. (i.e, through navigation.navigate(param, {...}) or navigation.push(param, {...}). Once I move to another page, some change in variable happened, and now I would like to send back the param with new data to the first page. I considered doing navigation.navigate(param, {...}) or navigation.push(param, {...}) again as it looks I cannot send back any parameters using goBack() or popToTop(), according to this
I checked this, but I am not 100% sure as I think pages might be stacked a lot if a user does the above actions many times.
Lets take an example
Think you have screens A,B and C in the stack and A is the home screen.
The actual stack will be an object but for easy understanding i'm using a simple array.
When you start the stack will be [A]
When you do a navigate to B the stack will be [A,B]
And if you push C to the stack from B then it will be [A,B,C]
Now all this is common but now if you do a navigate to B from C
then it will unmount C and go back to B and the stack will be [A,B]
If you chose push then it will add a new screen to the stack and stack will be [A,B,C,B] Notice that push always adds a new screen to the stack.
Ignore the push and assume that the stack is [A,B,C]
Now if you do goBack from C then it will pop just like the navigate method and go back to B.
But if you do popToTop it will unmount both C and B and make the stack look like this [A].
The difference is that goBack and popToTop does not pass parameters like navigate and push.
There is a way to achieve the same result of popToTop and goBack using navigate and useNavigationState.
The useNavigationState hook will get you the current navigation state which will have the information of all the screens in the stack. The sample navigation state value would be like this
{
stale: false,
type: 'stack',
key: 'stack-A32X5E81P-B5hnumEXkbk',
index: 1,
routeNames: ['Home', 'Details', 'MyView', 'ExtView'],
routes: [
{ key: 'Home-y6pdPZOKLOPlaXWtUp8bI', name: 'Home' },
{
key: 'MyView-w-6PeCuXYrcxuy1pngYKs',
name: 'MyView',
params: { itemId: 86, otherParam: 'anything you want here' },
},
],
}
As you can see you have the option to use this information to navigate to any screen in the stack. The navigate method can be used like below as well
navigation.navigate({ key: navState.routes[0].key, params: { id: 12 } })
If you use the key 0 then you will be taken to root along with a parameter and it will unmount the screen in the middle.
If you want to go back you can simply do an index - 1 which will give the same effect as goBack
navigation.navigate({ key: navState.routes[navState.Index-1].key, params: { id: 12 } })
So your requirement can be achieved.

Navigate.push action rendering the page multiple times

I am working on React Native Application. In my sidebar (Drawer) menu to open the categories I used
const pushAction = StackActions.push({
routeName: 'CategoryList',
params: {
catId: 9,
},
});
this.props.navigation.dispatch(pushAction);
This logic is working fine. But, when I click on categories in drawer on first click page is rendered only minimum times that's needed. But, again when I click on drawer menu category page rendered increased by two times every time. Due to this multiple loaders are showing up. I cannot be able to find out the best solution for this problem yet.

react-navigation: have the same state as a previous route, but act as a push?

Say I have Screen 1 and Screen 2. I navigate from Screen 1 to Screen 2 by pushing. So Screen 1 is still there, and its state is unchanged. But, I want to be able to navigate to a third screen, Screen 3, that has EXACTLY the same state as Screen 1 (a copy), but acts like a push.
Pushing from Screen 1 to Screen 2 gives me a card transition, but I basically want to push from Screen 2 to Screen 3 (another card transition), but have Screen 3 to have the same state as Screen 1, like scrolling is the same, data is the same, etc.
EDIT: Unfortunately, I cannot upload any gifs for clarification because many of them are too large. An example would be from the Reddit app: moving from the tabular view to viewing a post (where the tab view disappears), but then when you click on the subreddit's name, it takes you (with a push animation) to another tab view, but it's the same one.
EDIT 2: Here is a link of a screen recording of what I want.
So the first screen shows me scrolling down to a certain area. (tabular view) - Screen 1
Then there's a card navigation to a "View Post Screen", where you can see comments, etc. (no tabs) - Screen 2
Then I clicked on r/aww, which navigates with a card transition to the subreddit page. (tabular view) - Screen 3
The tab for Screen 3 is scrolled to the same place as I scrolled in Screen 1. This is what I meant by "same state".
Pushing another route of the same name doesn't work, because everything is set back to default, and would be bad for user experience. Going back wouldn't work because it would show a card "go back" transition from Screen 2 to Screen 3 - a card "push" transition is more natural.
In react-navigation you can navigate to unlimited - in theory - of same screen. If you wish to change the contents of the screen you can use navigation parameters. You can think instagram app for example.
User(me) -> Followers -> User(Alice) -> Followers -> User(Bob) -> Followers -> User(John)
const Navigator = createStackNavigator({
Followers: { screen: Followers },
User: { screen: Profile },
});
/...
this.props.navigation.navigate('User', { user: 'me' });
this.props.navigation.navigate('Followers', { user: 'me' });
this.props.navigation.navigate('User', { user: 'Alice' });
this.props.navigation.navigate('Followers', { user: 'Alice' });
this.props.navigation.navigate('User', { user: 'Bob' });
this.props.navigation.navigate('Followers', { user: 'Bob' });
this.props.navigation.navigate('User', { user: 'John' });
My understanding of your use case scenario is that you want to preserve the styling and structure of a component across different screens. I have two takeaways here:
If the two screens (Screen1 and Screen3) are exactly the same, then why would you want a duplicate in the first place? Why not show the Screen1 again to keep your stack size small? In your example: User is on Screen1 which is subreddit's homepage. Then they click on a post link which takes them to Screen2. Again, when user clicks on the link to subreddit's homepage, they are not opening a new screen, they are simply going back to the original Screen1.
If there are slight differences between the two screens, your could simply employ component re-use. Like, if there are two subreddits r/android and r/reactnative, you'll have a single component for both of them: <SubRedditComponent> but their data and styling would be different.