React Navigation - TopTabNavigator inside DrawerNavigator - react-navigation

react-navigation: 3.2.1
I've create TopTabNavigator (using createMaterialTopTabNavigator) inside a Drawer (using createDrawerNavigator).
Swiping navigates between tabs but the first time I swipe from the direction of the drawer - the drawer will be the only thing that will open / close.
swiping between the tabs is stopped (click the buttons in the bar still works).
What can cause that?
const MainTab = createMaterialTopTabNavigator ({
Tab1: {
screen: Tab1Screen,
},
Tab2: {
screen: Tab2Screen,
},
});
const DrawerNav = createDrawerNavigator(
Main: {
screen: MainTab,
}
);
Adding link for Expo - but unfortunately there is a bug when using createDrawerNavigation inside expo.snack.io

Related

Next.js: Get back to old scroll position on back button WITHOUT affecting the other/next page scroll?

Is there a possibility in next.js to get back to old scroll position on back button WITHOUT affecting the other/next page scroll?
The solution I tried is working but not without affecting the next page scroll position and also the next page affecting the previous/back button page scroll postion.
So if i do not scroll on next page and go back it's fine, but I scroll on next page and get back the position of back page(original page) is not the same
What I tried:
Update next.config.js with:
experimental: {
modern: true,
scrollRestoration: true,
}
Switched scroll prop to false for SiteLink component which wraps Link from next:
<SiteLink href={slug} scroll={false} />
Attach and remove scroll event listener on page I want scroll to be remebered:
const state = useContext(ScrollContext);
useEffect(() => {
window.scrollTo(0, state.scrollPos);
const handleScrollPos = () => {
state.scrollPos = window.scrollY;
};
window.addEventListener('scroll', () => handleScrollPos());
return () => {
// remove event listener on unmount
window.removeEventListener('scroll', () => handleScrollPos());
};
}, []);
Scroll Context
import { createContext } from 'react';
const state = {
scrollPos: 0,
};
const ScrollContext = createContext(state);
export default ScrollContext;```

How can I navigate to a nested stack screen inside a sibling tab while preserving its initial screen? (React Navigation)

The Code:
I'm using React Navigation 6 with the following hierarchy:
MainTabNavigator
HomeStack
HomeScreen (HomeStack initial screen, contains a "Pay" button)
OtherScreen
MembershipStack
MembershipHomeScreen (MembershipStack initial screen)
PayMembershipScreen (should always navigate back to MembershipHomeScreen)
The App launches into HomeScreen (inside HomeStack), so the MembershipStack tab won't be loaded yet.
There is a "Pay" button inside HomeScreen that redirects to PayMembershipScreen inside MembershipStack.
Code of the HomeScreen with the "Pay" button press handler:
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
// The parent navigator here is MainTabNavigator
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
The "Pay" button inside HomeScreen does navigate to PayMembershipScreen with this code.
The Problem:
When the MembershipStack tab has not yet been loaded (i.e. when the App just launched), the following happens:
User clicks the "Pay" button on HomeScreen.
App navigates to PayMembershipScreen.
PayMembershipScreen becomes the initial screen on MembershipStack.
User can't go back to MembershipHomeScreen from PayMembershipScreen.
The question is: how can I navigate from HomeScreen directly to PayMembershipScreen and after that be able to go back to MembershipHomeScreen (i.e. have it available as the initial screen in the MembershipStack history)?
What I've tried so far:
1. Setting lazy: false on the MainTabNavigator options.
This approach does make sure that MembershipHomeScreen is always the initial screen on MembershipStack, since all stacks (and their initial screens) will be loaded when the App launches. However this comes with a noticeable drawback in performance, since there's at least 5 tabs in the actual project.
Also, since MembershipHomeScreen is already focused inside MembershipStack, there's a weird MembershipHomeScreen to PayMembershipScreen transition animation when the "Pay" button on HomeScreen is pressed. I just want the user to see a transition from HomeScreen to PayMembershipScreen at most, nothing flashing inbetween.
2. Define a param on MembershipHomeScreen to indicate when I want to redirect to PayMembershipScreen.
On this approach, I'm using a boolean param on MembershipHomeScreen called redirectToPayment:
Code inside MembershipHomeScreen:
const MembershipHomeScreen = ({navigation, route}) => {
const { redirectToPayment = false } = route.params
const redirectIfNeeded = useCallback(() => {
if (redirectToPayment) {
// reset the param
navigation.setParams({ redirectToPayment: false })
// redirect to the desired screen
navigation.navigate("PayMembershipScreen")
}
}, [redirectToPayment, navigation])
// Using layout effect to avoid rendering MembershipHomeScreen when redirecting.
useLayoutEffect(redirectIfNeeded)
return (
<Text>
This is just the membership home screen,
not the target screen of the "Pay" button.
</Text>
)
}
And on the HomeScreen:
const payPressHandler = useCallback(() => {
navigation
.getParent()
.navigate(
"MembershipStack",
{ screen: "MembershipHomeScreen", params: { redirectToPayment: true } }
)
}, [navigation])
The use of React's useLayoutEffect comes with the known drawback of freezing the screen since it will leave any rendering tasks on hold while it's running. I'm able to notice a 2 seconds freeze in the HomeScreen when I press the "Pay" button on a 4GB RAM Moto G7...
...and even after using useLayoutEffect, the MembershipHomeScreen still renders nonetheless to show a transition animation between MembershipHomeScreen and PayMembershipScreen.
Same behavior with useEffect, except it renders the MembershipHomeScreen instead of a 2 seconds freeze.
3. Using React Navigation's dispatch function to customize the route history.
On this approach, I intend to dispatch a custom action that does the following to the navigation state of MainTabNavigator:
Before:
index: 0
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home" // <-- currently active screen
}
]
}
},
1: {
name: "MembershipStack",
state: undefined // <-- this is an unloaded tab
}
]
After:
index: 1
routes: [
0: {
name: "HomeStack",
state: {
index: 0,
routes: [
0: {
name: "Home"
}
]
}
},
1: {
name: "MembershipStack",
state: {
index: 1,
routes: [
0: {
name: "MembershipHomeScreen"
},
1: {
name: "PayMembershipScreen" // <-- currently active screen
}
]
}
},
]
Here's the code I'm using inside the HomeScreen for that:
const payPressHandler = useCallback(() => {
navigation.getParent().dispatch(state => {
const membershipTabIndex = state.routes.findIndex(r => r.name === "MembershipStack")
// get the current navigation state inside the MembershipStack
let membershipTabState = state.routes[membershipTabIndex].state
// point to PayMembershipScreen without overriding the initial screen on that tab
if (!membershipTabState) {
// tab is unloaded, so just set the ideal state
membershipTabState = { index: 1, routes: [{ name: "MembershipHomeScreen" }, { name: "PayMembershipScreen" }] }
} else {
// tab already has a navigation state, so we'll point to PayMembershipScreen
// if it's loaded in the stack. Otherwise, we'll add it and point to it.
let payMembershipScreenIndex = membershipTabState.routes.findIndex(r => r.name === "PayMembershipScreen")
if (payMembershipScreenIndex === -1) {
payMembershipScreenIndex = membershipTabState.routes.push({ name: "PayMembershipScreen" }) - 1
}
membershipTabState.index = payMembershipScreenIndex
}
// update the MembershipStack tab with the new state
const routes = state.routes.map((r, i) => i === membershipTabIndex ? { ...r, state: membershipTabState} : r)
// update the MainTabNavigator state
return CommonActions.reset({
...state,
routes,
index: membershipTabIndex
})
})
}, [navigation])
That code almost accomplishes the expected outcome:
It navigates from HomeScreen to PayMembershipScreen successfuly.
The user can go back to MembershipHomeScreen from PayMembershipScreen.
MembershipHomeScreen does not render in-between during the transition.
However:
It won't work a second time if you do go back to MembershipHomeScreen from PayMembershipScreen.
Turns out, if I go back to MembershipHomeScreen once I'm inside PayMembershipScreen, then go back to the HomeStack and press the "Pay" button again, it will now navigate to MembershipHomeScreen instead of PayMembershipScreen.
Additionally, the MembershipHomeScreen will now display a disabled back button in the header (probably a bug).
This last approach is so far the closest to getting the desired outcome, so I really hope that it only needs a fix in the logic and it's not really a bug.
Summary:
Is anyone able find a solution that achieves the expected outcome? To sum up:
It should make the "Pay" button navigate from HomeScreen to PayMembershipScreen.
The user should be able to go back to MembershipHomeScreen once they're in PayMembershipScreen.
The MembershipHomeScreen should not flash in the transition from HomeScreen to PayMembershipScreen.
The screen should not freeze inbetween the transitions (no use of useLayoutEffect).
Don't load all tabs on App launch (it's too much burden on the actual 5-tab project).
If the user navigates back to MembershipHomeScreen once they're inPayMembershipScreen, pressing the "Pay" button on HomeScreen again should open the PayMembershipScreen again (no buggy behavior).
Minimal reproducible example:
Here's a snack with all of the approaches mentioned. Please check it out and use it as a playground for your solution!
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button
Solution
I got the expected outcome with the following approach:
Navigate directly to PayMembershipScreen when the "Pay" button is pressed.
Inside PayMembershipScreen, add MembershipHomeScreen to the top of MembershipStack history if not already present.
The Code
Code inside HomeScreen (no changes):
const HomeScreen = ({navigation}) => {
const payPressHandler = useCallback(() => {
navigation.getParent().navigate("MembershipStack", { screen: "PayMembershipScreen" })
}, [navigation])
return (
<TouchableOpacity onPress={payPressHandler}>
<Text>Go to Pay screen!</Text>
</TouchableOpacity>
)
}
New useAssertInitialScreen hook:
const useAssertInitialScreen = ({ navigation, name }) => {
useEffect(() => {
if (navigation.canGoBack() === false) {
navigation.dispatch((state) => {
const routes = [{ name }, ...state.routes]
return CommonActions.reset({
...state,
routes,
index: state.index + 1
})
})
}
}, [navigation])
}
Note: I'm passing the navigation prop down the hook since it's also used in the screen, but it could also be obtained from React Navigation's useNavigation hook.
Code inside PayMembershipScreen:
const PayMembershipScreen = ({navigation}) => {
useAssertInitialScreen({ navigation, name: "MembershipHomeScreen" })
return (
<View>
<Text>
This is the PayMembershipScreen AKA the target
</Text>
</View>
)
}
Demo
Here's a snack with the code of this answer:
https://snack.expo.dev/#ger88555/tabs-with-stacks-and-a-button---a-solution
I still want to see whether there's a cleaner solution than handling the "Pay" button's functionality inside its target screen.
I'll mark as the accepted answer any other solution that gets the expected outcome :)

How to Unmount a screen when moving to another in React Native

I'm developing a React Native app using React Navigation v4, React Hooks and ES6.
I have 2 bottom tabs (Movies, Shows) and 4 screens with the following Stack structure:
**Movies**
-- MovieList
-- MovieDetail
**Shows**
-- ShowList
-- ShowDetail
My scenario
1) Moving from Movie list to an individual movie page
MovieList contains a list of movies, when I click on one of them, I first fetch some API data then move to the MovieDetail screen like this
dispatch(apiFetchActions.fetchMovies(movieId)).then((response) => {
props.navigation.navigate({
routeName: "MovieDetail",
params: {
assetId: movieId,
assetName: movieTitle,
},
});
MovieDetail is now on top of the Movies stack and MovieList at the bottom
2) Moving to a different tab (navigation stack)
I then click on Shows (2nd Tab) which takes me to the ShowList using props.navigation.navigate('ShowList')
3) The problem
If I click on the Movies Tab, I expect to be moved back to MovieList but since MovieDetail was never unmounted, it is still at the top of the Movies stack meaning that I see an old screen. I have to click twice to go back to the MovieList screen.
I've read quite a few suggestions on how to use onFocus/onBlur subscription however I could not found a solution using React Hooks.
My ideal solution would be to find a way to listen to the onBlur status in MovieDetail screen possibly using useEffect hook and somehow unmount it before leaving.
I found a way to make it easier to always move to the initial top of the stack when you click on any bottom tab icons.
You simply need to add the on Press and the screen reference like this
Stars: {
screen: StarsNavigator,
navigationOptions: ({ navigation }) => ({
tabBarIcon: (tabInfo) => {
return (
<Ionicons name="ios-people" size={22} color={tabInfo.tintColor} />
);
},
tabBarLabel: Platform.OS === "android" ? <Text>Stars</Text> : "Stars",
tabBarOnPress: () => {
navigation.navigate("StarsList");
},
}),
},
Star is one of my screens in BottomTabNavigator and using navigation.navigate("You Screen") does the trick. So regardless in which level of the stack you find yourself, every time you click on the Star tab you always end up to the original top level.

How we can use swipe in react navigation V3 with bottomTabNavigator

I'm using react navigation V3 and i want to use swipe in my bottomTabNavigator.
i can't do it because createBottomTabNavigator don't support it any yet and createBottomNavigator is actually deprecated.
It is very annoying because in react navigation V2 we can do iteasily.
Just createMaterialTopTabNavigator support swipe but i want a bottom navigator and not a top one
If you take a look at the documentation for createMaterialTopTabNavigator you can see that in the TabNavigatorConfig there is the ability to set the position of the tab bar using tabBarPosition
Position of the tab bar, can be 'top' or 'bottom', default is top
So if you use createMaterialTopTabNavigator instead of createMaterialBottomTabNavigator and set tabBarPosition: 'bottom' in your config you should get a createMaterialTopTabNavigator but at the bottom.
Here is what it should look like in code
import Screen1 from './Screen1';
import Screen2 from './Screen2';
import { createMaterialTopTabNavigator, createAppContainer } from 'react-navigation';
const screens = {
Screen1: {
screen: Screen1
},
Screen2: {
screen: Screen2
}
}
const config = {
headerMode: 'none',
initialRouteName: 'Screen1',
tabBarPosition: 'bottom' // <- add this line to your config
}
const MainNavigator = createMaterialTopTabNavigator(screens,config);
export default createAppContainer(MainNavigator);
Here is a snack showing it working https://snack.expo.io/#andypandy/materialtopnavigator-at-the-bottom

How to slide up/down on page navigation in Ionic 3?

When I navigate from page A to page B, I want to slide in the page B from the bottom upwards.
How can I do this in Ionic 3?
I have now
this.navCtrl.push(PageB,
{session: this.session},
{animate: true,
animation: 'transition',
duration: 500,
direction: 'forward'}
);
I tried to change the 'forward' in 'up' but that does not do anything.
Currently I am testing in Chrome browser.
Thanks
I think the navController is the wrong choice for this.
Mostly, the use case for sliding a page from bottom to top is when you want to show modal page. This can be done with the ModalController:
constructor(private modalCtrl: ModalController) {
}
showModal(): void {
let modal = this.modalCtrl.create(PageB, options);
modal.present();
}

Resources