Implementing UISplitViewController using React Navigation - react-native

I need to implement tablet support in the React Native app I am working on. We decided that going with UISplitViewController makes the most sense for us, but React Navigation (https://reactnavigation.org) does not have any support for it.
I tried solving this problem in a straightforward way by putting 2 navigators side-by-side in a view and modifying their getStateForAction to open certain screens in the details view:
export interface Props {
MasterNavigator: NavigationContainer;
DetailNavigator: NavigationContainer;
detailRouteNames: string[];
}
export class MasterDetailView extends React.Component<Props> {
masterNavigator: any;
detailNavigator: any;
componentDidMount() {
const { MasterNavigator, DetailNavigator, detailRouteNames } = this.props;
const defaultMasterGetStateForAction = MasterNavigator.router.getStateForAction;
MasterNavigator.router.getStateForAction = (action: any, state: any) => {
if (action.type === NavigationActions.NAVIGATE && detailRouteNames.indexOf(action.routeName) !== -1) {
action.params.isRootScreen = true;
this.detailNavigator.dispatch(NavigationActions.reset({ index: 0, actions: [action] }));
return state;
}
return defaultMasterGetStateForAction(action, state);
};
const defaultDetailGetStateForAction = DetailNavigator.router.getStateForAction;
DetailNavigator.router.getStateForAction = (action: any, state: any) => {
if (action.type === NavigationActions.BACK && state.routes.length === 1) {
this.masterNavigator.dispatch(NavigationActions.back());
return null;
}
return defaultDetailGetStateForAction(action, state);
};
}
render() {
const { MasterNavigator, DetailNavigator } = this.props;
return (
<View style={styles.view}>
<View style={styles.masterContainer}>
<MasterNavigator ref={(mn: any) => this.masterNavigator = mn}/>
</View>
<View style={styles.divider}/>
<View style={styles.detailContainer}>
<DetailNavigator ref={(dn: any) => this.detailNavigator = dn}/>
</View>
</View>
);
}
}
The master navigator is a TabNavigator with StackNavigators in each tab, the detail navigator is a StackNavigator. Here's how it looks:
This approach kind of works, but back button on Android behaves incorrectly. I want it to navigate back in the details navigator, then in the master navigator. It is possible to make it work with a couple of hacks, but then it becomes impossible to navigate back from the app using the back button (for some reason, nothing happens when I click it). I could try fix this by overriding the behavior of back button and dispatching back action to the master/detail navigators or closing the app, but when dispatching an action to the navigator, there is no way to know whether it responded to it or not (particularly with the master navigator, which is a TabNavigator), so it seems like I am stuck.
Are there some special considerations when using navigators this way? Is React Navigation even suitable for this use case? If not, what are the other ways to emulate UISplitViewController in React Native apps?

The problem with incorrect back button behavior was my fault. One of the screens had a back button handler which always returned true, thus consuming all back button presses (duh!). I fixed this and added the following back button handler to the MasterDetailView:
private onPressBack = (): boolean => {
const action = NavigationActions.back();
return this.detailNavigator.dispatch(action) || this.masterNavigator.dispatch(action);
};
And yes, there is a way to find out whether a navigator consumed an action: the navigator.dispatch(action) returns true if the event was consumed and false otherwise.
Once the problem with back button is fixed, this setup emulates the UISplitViewController pretty well.

Related

OnDidFocus event not working when you navigate back from the stack

I'm trying to test the OnDidFocus event in my React Native app using react navigation 4 and using the following event listener:
useEffect(() => {
const willFocusSub = props.navigation.addListener(
"onDidFocus",
console.log("testing onDidFocus")
);
return () => {
willFocusSub.remove();
};
});
When I first load the page it works fine but when I move away and then come back to the same screen through the Back button it does not seem to perceive the focus event.
This is my stack
const MovieNavigator = createStackNavigator(
{
MoviesList: HomeMovies,
MovieDetail: MovieDetailScreen,
PopularMovies: PopularMoviesScreen,
CrewMember: CastDetailScreen,
GenreSearch: GenreSearchScreen,
MovieSearch: MovieSearchScreen,
},
I'm in MoviesList and the event is triggered fine, then I move to MovieDetail. If I hit Back and return to MoviesList the event onDidFocus is not triggered at all.
I think you could try "willFocus" instead.
Like this:
const willFocusSub = props.navigation.addListener(
"willFocus",
()=>{console.log("testing willFocus")}
);
Try modyfying your useEffect call to this!
useEffect(() => {
const willFocusSub = props.navigation.addListener(
"onDidFocus",
console.log("testing onDidFocus")
);
return () => {
willFocusSub.remove();
};
},[]);
I found another way to detect the focus and blur event and seems the only way to track an event when using the Back button.
Instead of subscribing to events, I'm check the focus status of the screen using the useIsFocused() hooks available from react-navigation-hooks library.
import { useIsFocused } from "react-navigation-hooks";
...
const [showGallery, setShowGallery] = useState(true);
...
useEffect(() => {
if (isFocused) {
setShowGallery(true);
} else {
setShowGallery(false);
}
console.log("isFocused: " + isFocused);
}, [isFocused]);
Basically I'm checking the status of the screen using isFocused hook every time it changes (when it leaves and returns only same as didFocus and didBlur) and setting the state setShowGallery accordingly to run the carousel when focused and stop it when blurred.
Hope it helps others!

Deep linking into an already open screen with different params. Is it possible?

I'm working on an app which has a category screen where the relevant posts of the category are displayed. I'm using react-navigation to navigate between the screens and handle the deep linking. The category screen can be accessed in-app or via deep link. To access the category screen via deep link I'm using something like myapp://category/:id.
If the app is already opened and is focused on the category screen the deep link does nothing.
I've currently "fixed" it using the componentDidUpdate life cycle method to compare the ID stored in the state and the ID in navigation.getParam.
componentDidUpdate = () => {
const { navigation } = this.props;
const { categoryID } = this.state;
const paramID = navigation.getParam('id', null);
if (paramID !== categoryID) {
this.setState({ categoryID: paramID }, () => {
// fetch data
});
}
}
However, this seems like a dirty fix to me. Is there a better way of doing this? Maybe opening all deep links using push instead of navigate via react-navigation?
For anyone who got here wondering how this can be done, like myself, here's the solution:
You can use the getId Stack.Screen property. You just need to return the unique ID from the getId property. Here's an example:
Let's say we have a UserScreen component, which is part of our Stack navigator. The UserScreen has one route param: userId. We can use the getId property like this:
import { createStackNavigator } from '#react-navigation/stack';
const Stack = createStackNavigator();
const AppStack = () => {
const getUserRouteId = ({ params }) => {
return params && params.userId;
};
return (
<Stack.Navigator>
{/* Other routes */}
<Stack.Screen name="User" component={UserScreen} getId={getUserRouteId} />
</Stack.Navigator>;
);
};
Now when you try to normally navigate to this route, using the navigate function, if the param userId in the navigation action is different than on the UserScreen which you're currently on, it will navigate you to a new user screen. React Navigation deep linking uses the navigate function internally.

Issue with Unstated and React Navigation in React Native

I have the function
onPress = (store) => {
//store.flipState();
this.props.navigation.navigate('anotherScreen');
console.log('hi');
}
If I run it as above the navigation works.
If I uncomment the store.flipState() line the state changes but the navigation doesn't work (the screen just refreshes).
The console.log works in both cases.
How can I change the state and navigate at the same time?
I use Unstated and React Navigation in React Native.
Thank you.
I know this is really old, but what if you pass the navigate action to flipState
const {navigation: {navigate}} = this.props
store.flipState(navigate('anotherScreen'))
then, in flipState, when you call setState, pass the navigate as the success action callback
flipState = (callback) => {
this.setState((state) => {
return { flippedState: !state.flippedState };
}, callback);
};

How to avoid navigating to other screen multiple times

When press on any button on my React Native App to navigate to a different screen multiple times, then it will redirected to the next screen multiple times.
My sample code is:
// This is my button click event
myMethod()
{
this.props.navigation.navigate("ScreenName")
}
I am using react-navigation to navigate through my app.
How can I fix this behaviour?
I think there are a few ways this could be done. Perhaps recording when the navigation has occurred and preventing it from navigating multiple times.
You may also want to consider resetting hasNavigated after an amount of time etc as well.
// Somewhere outside of the myMethod scope
let hasNavigated = false
// This is my button click event
myMethod()
{
if (!hasNavigated) {
this.props.navigation.navigate("ScreenName")
hasNavigated = true
}
}
This react-navigation issue contains a discussion about this very topic, where two solutions were proposed.
The first, is to use a debouncing function such as Lodash's debounce that would prevent the navigation from happening more than once in a given time.
The second approach, which is the one I used, is to check on a navigation action, whether it is trying to navigate to the same route with the same params, and if so to drop it.
However, the second approach can only be done if you're handling the state of the navigation yourself, for example by using something like Redux.
Also see: Redux integration.
One of solution is custom custom components with adds debounce to onPress:
class DebounceTouchableOpacity extends Component {
constructor(props) {
super(props);
this.debounce = false;
}
_onPress = () => {
if (typeof this.props.onPress !== "function" || this.debounce)
return;
this.debounce = true;
this.props.onPress();
this.timeoutId = setTimeout(() => {
this.debounce = false;
}, 2000);
};
componentWillUnmount() {
this.timeoutId && clearTimeout(this.timeoutId)
}
render() {
const {children, onPress, ...rest} = this.props;
return (
<TouchableOpacity {...rest} onPress={this._onPress}>
{children}
</TouchableOpacity>
);
}
}
another: wrap onPress function into wrapper with similar behavior
const debounceOnPress = (onPress, time) => {
let skipCall = false;
return (...args) => {
if (skipCall) {
return
} else {
skipCall = true;
setTimeout(() => {
skipCall = false;
}, time)
onPress(...args)
}
}
}

While coming back from react navigation componentWillMount doesn't get called

I have used react-navigation and on clicking hardware back button in android, I come back to previous component but componentWillMount doesn't get called. How do I ensure that componentWillMount is called?
componentWillMount will not trigger when you entering new screen / back to the screen.
my solution is using event navigator handler
https://wix.github.io/react-native-navigation/#/screen-api?id=listen-visibility-events-in-onnavigatorevent-handler
you can implement your 'componentWillMount' codes while 'willAppear' event id triggered, see this implementation:
export default class ExampleScreen extends Component {
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
switch(event.id) {
case '`willAppear`':
// { implement your code on componentWillMount }
break;
case 'didAppear':
break;
case 'willDisappear':
break;
case 'didDisappear':
break;
case 'willCommitPreview':
break;
}
}
}
Does this answer from #bumbur help you? It defines a global variable that tracks if nav state has changed. You could insert a piece of code to see if you're in the specific tab that you are interested in. With that you could trigger a call to componentWillMount() ?
If you don't want to use redux, this is how you can store globally
information about current route, so you can both detect a tab change
and also tell which tab is now active.
https://stackoverflow.com/a/44027538/7388644
export default () => <MyTabNav
ref={(ref) => { this.nav = ref; }}
onNavigationStateChange={(prevState, currentState) => {
const getCurrentRouteName = (navigationState) => {
if (!navigationState) return null;
const route = navigationState.routes[navigationState.index];
if (route.routes) return getCurrentRouteName(route);
return route.routeName;
};
global.currentRoute = getCurrentRouteName(currentState);
}}
/>;