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

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.

Related

Reactnavigation params error "undefined is not an object evaluating route.params"

I get the error mentioned in the title and solutions like this one do not seem to work.
The user starts on screen 1 and then goes to several other screens before ending up on screen 1 again. When the user goes to screen 1 (again) I want to pass some params (and somehow use thos params to force a re-render of that component). This is my code:
Screen 1
function Screen 1(props, { route, navigation }) {
...
const { itemId } = route.params;
console.log(itemId);
Screen X (the last screen the user visits before going back to screen 1)
onPress={() => {
props.navigation.navigate("Screen_1", { itemId: Doe });
}}
You cannot mix destructuring of props and the props object at the same time, hence the statement
function Screen1(props, { route, navigation }) { ... }
is not valid.
You need to either destructure everything you need from props or use the props object.
function Screen1({ route, navigation }) {
const { itemId } = route.params
}
or
function Screen1(props) {
const { itemId } = props.route.params
}

How to skip nested navigator's initial route?

I have a navigation like so:
Loading: SwitchNavigator {
Auth: Stacknavigator,
Main: StackNavigator,
Onboard: StackNavigator,
}
every one of StackNavigators has an initial route set. Under certain circumstances, I want to go from loading navigator to a specific route on onboard navigator. If I use just navigator.navigate('DesiredRoute'), it squeezes in also onboard's initialRoute, so the navstack looks like [InitialRoute, DesiredRoute] instead of [DesiredRoute]. How to get rid of the InitialRoute?
Code example:
// Loading.js
if (loadingComplete) {
if (!user) {
navigation.navigate('auth')
return
}
if (user && userData) {
navigation.navigate('Main')
return
}
if (onboardingCheckpointCleared) {
// this creates `['InitialRoute', 'DesiredRoute']` instead of `['DesiredRoute']`
navigation.navigate('DesiredRoute')
return
}
navigation.navigate('Onboard')
}
This is expected behavior, if you want only DesiredRoute to appear then you have to set that route in Loading as well like below,
Loading: SwitchNavigator {
Auth: Stacknavigator,
Main: StackNavigator,
Onboard: StackNavigator,
DesiredRoute: ScreenName
}
Writing like this will not create any clash, you are safe to write.
I assume you are navigating to DesiredRoute from outside the Onboard stack navigator
If you're outside the Onboard navigator, doing navigation.navigate('DesiredRoute') will trigger two actions: first, it will navigate to the Onboard stack navigator (so it will also activate the default sub screen of it InitialRoute like you call it) and then it will push the DesiredRoute. If you want to go to Onboard with only DesiredRoute on the stack, you can use the sub actions of the navigation actions like this:
navigation.navigate('Onboard', undefined, StackActions.replace('DesiredRoute'));
The third argument is an action that can be will be executed by the first argument navigator (if the first argument is not a screen but a navigator). Here it will navigate to the Onboard navigator and thus put the InitialRoute in the stack (automatically as it's the initialRoute of Onboard). However, the StackAction.replace('DesiredRoute') will be executed by the Onboard navigator and will replace InitialRoute with DesiredRoute.
See the official doc about the navigate: https://reactnavigation.org/docs/en/navigation-prop.html#navigate-link-to-other-screens
I ended up creating a custom router that strips out the initial route when navigating to my nested stack:
const MyStackNav = createStackNavigator({ ...routes })
const defaultGetStateForAction = MyStackNav.router.getStateForAction
const customGetStateForAction = (action, state) => {
const defaultNavState = defaultGetStateForAction(action, state)
// If we're pushing onto a stack that only has a defaulted initialRoute
if (
!!defaultNavState &&
!defaultNavState.routeName &&
defaultNavState.isTransitioning &&
defaultNavState.index === 1 &&
action.type === NavigationActions.NAVIGATE
) {
const newState = {
...defaultNavState,
index: 0, // Decrement index
routes: defaultNavState.routes.slice(1), // Remove initial route
}
return newState
}
return defaultNavState
}

React-navigation: Deep linking with authentication

I am building a mobile app with react-native and the react-navigation library for managing the navigation in my app. Right now, my app looks something like that:
App [SwitchNavigator]
Splash [Screen]
Auth [Screen]
MainApp [StackNavigator]
Home [Screen] (/home)
Profile [Screen] (/profile)
Notifications [Screen] (/notifications)
I have integrated Deep Linking with the patterns above for the screens Home, Profile and Notifications, and it works as expected. The issue I am facing is how to manage my user's authentication when using a deep link. Right now whenever I open a deep link (myapp://profile for instance) the app takes me on the screen whether or not I am authenticated. What I would want it to do is to check before in AsyncStorage if there is a userToken and if there isn't or it is not valid anymore then just redirect on the Auth screen.
I set up the authentication flow in almost exactly the same way as described here. So when my application starts the Splash screen checks in the user's phone if there is a valid token and sends him either on the Auth screen or Home screen.
The only solution I have come up with for now is to direct every deep link to Splash, authentify my user, and then parse the link to navigate to the good screen.
So for example when a user opens myapp://profile, I open the app on Splash, validate the token, then parse the url (/profile), and finally redirect either to Auth or Profile.
Is that the good way to do so, or does react-navigation provide a better way to do this ? The Deep linking page on their website is a little light.
Thanks for the help !
My setup is similar to yours. I followed Authentication flows · React Navigation and SplashScreen - Expo Documentation to set up my Auth flow, so I was a little disappointed that it was a challenge to get deep links to flow through it as well. I was able to get this working by customizing my main switch navigator, the approach is similar to what you stated was the solution you have for now. I just wanted to share my solution for this so there’s a concrete example of how it’s possible to get working. I have my main switch navigator set up like this (also I’m using TypeScript so ignore the type definitions if they are unfamiliar):
const MainNavigation = createSwitchNavigator(
{
SplashLoading,
Onboarding: OnboardingStackNavigator,
App: AppNavigator,
},
{
initialRouteName: 'SplashLoading',
}
);
const previousGetActionForPathAndParams =
MainNavigation.router.getActionForPathAndParams;
Object.assign(MainNavigation.router, {
getActionForPathAndParams(path: string, params: any) {
const isAuthLink = path.startsWith('auth-link');
if (isAuthLink) {
return NavigationActions.navigate({
routeName: 'SplashLoading',
params: { ...params, path },
});
}
return previousGetActionForPathAndParams(path, params);
},
});
export const AppNavigation = createAppContainer(MainNavigation);
Any deep link you want to route through your auth flow will need to start with auth-link, or whatever you choose to prepend it with. Here is what SplashLoading looks like:
export const SplashLoading = (props: NavigationScreenProps) => {
const [isSplashReady, setIsSplashReady] = useState(false);
const _cacheFonts: CacheFontsFn = fonts =>
fonts.map(font => Font.loadAsync(font as any));
const _cacheSplashAssets = () => {
const splashIcon = require(splashIconPath);
return Asset.fromModule(splashIcon).downloadAsync();
};
const _cacheAppAssets = async () => {
SplashScreen.hide();
const fontAssetPromises = _cacheFonts(fontMap);
return Promise.all([...fontAssetPromises]);
};
const _initializeApp = async () => {
// Cache assets
await _cacheAppAssets();
// Check if user is logged in
const sessionId = await SecureStore.getItemAsync(CCSID_KEY);
// Get deep linking params
const params = props.navigation.state.params;
let action: any;
if (params && params.routeName) {
const { routeName, ...routeParams } = params;
action = NavigationActions.navigate({ routeName, params: routeParams });
}
// If not logged in, navigate to Auth flow
if (!sessionId) {
return props.navigation.dispatch(
NavigationActions.navigate({
routeName: 'Onboarding',
action,
})
);
}
// Otherwise, navigate to App flow
return props.navigation.navigate(
NavigationActions.navigate({
routeName: 'App',
action,
})
);
};
if (!isSplashReady) {
return (
<AppLoading
startAsync={_cacheSplashAssets}
onFinish={() => setIsSplashReady(true)}
onError={console.warn}
autoHideSplash={false}
/>
);
}
return (
<View style={{ flex: 1 }}>
<Image source={require(splashIconPath)} onLoad={_initializeApp} />
</View>
);
};
I create the deep link with a routeName query param, which is the name of the screen to navigate to after the auth check has been performed (you can obviously add whatever other query params you need). Since my SplashLoading screen handles loading all fonts/assets as well as auth checking, I need every deep link to route through it. I was facing the issue where I would manually quit the app from multitasking, tap a deep link url, and have the app crash because the deep link bypassed SplashLoading so fonts weren’t loaded.
The approach above declares an action variable, which if not set will do nothing. If the routeName query param is not undefined, I set the action variable. This makes it so once the Switch router decides which path to take based on auth (Onboarding or App), that route gets the child action and navigates to the routeName after exiting the auth/splash loading flow.
Here’s an example link I created that is working fine with this system:
exp://192.168.1.7:19000/--/auth-link?routeName=ForgotPasswordChange&cacheKey=a9b3ra50-5fc2-4er7-b4e7-0d6c0925c536
Hopefully the library authors will make this a natively supported feature in the future so the hacks aren’t necessary. I'd love to see what you came up with as well!
On my side I achieved this without having to manually parse the route to extract path & params.
Here are the steps:
getting the navigation action returned by: getActionForPathAndParams
passing the navigation action to the Authentication view as param
then when the authentication succeed or if the authentication is already ok I dispatch the navigation action to go on the intended route
const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams
Object.assign(AppContainer.router, {
getActionForPathAndParams(path: string, params: NavigationParams) {
const navigationAction = previousGetActionForPathAndParams(path, params)
return NavigationActions.navigate({
routeName: 'Authentication',
params: { navigationAction }
})
}
})
Then In the Authentication view:
const navigationAction = this.navigation.getParam('navigationAction')
if (navigationAction)
this.props.navigation.dispatch(navigationAction)
I ended up using a custom URI to intercept the deeplink launch, and then passing those params to the intended route. My loading screen handles the auth check.
const previousGetActionForPathAndParams = AppContainer.router.getActionForPathAndParams
Object.assign(AppContainer.router, {
getActionForPathAndParams (path, params) {
if (path === 'auth' && params.routeName && params.userId ) {
// returns a profile navigate action for myApp://auth?routeName=chat&userId=1234
return NavigationActions.navigate({
routeName: 'Loading',
params: { ...params, path },
})
}
return previousGetActionForPathAndParams(path, params)
},
})
https://reactnavigation.org/docs/en/routers.html#handling-custom-uris
Then, in my Loading Route, I parse the params like normal but then route to the intended location, passing them once again.
const userId = this.props.navigation.getParam('userId')
https://reactnavigation.org/docs/en/params.html
I found an easier way, i'm maintaining the linking in a separate file and importing it in the main App.js
linking.js
const config = {
screens: {
Home:'home',
Profile:'profile,
},
};
const linking = {
prefixes: ['demo://app'],
config,
};
export default linking;
App.js
& during login I keep the token inside async storage and when user logs out the token is deleted. Based on the availability of token i'm attaching the linking to navigation and detaching it using state & when its detached it falls-back to SplashScreen.
Make sure to set initialRouteName="SplashScreen"
import React, {useState, useEffect} from 'react';
import {Linking} from 'react-native';
import AsyncStorage from '#react-native-async-storage/async-storage';
import {createStackNavigator} from '#react-navigation/stack';
import {NavigationContainer} from '#react-navigation/native';
import linking from './utils/linking';
import {Home, Profile, SplashScreen} from './components';
const Stack = createStackNavigator();
// This will be used to retrieve the AsyncStorage String value
const getData = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
return value != null ? value : '';
} catch (error) {
console.error(`Error Caught while getting async storage data: ${error}`);
}
};
function _handleOpenUrl(event) {
console.log('handleOpenUrl', event.url);
}
const App = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
// Checks if the user is logged in or not, if not logged in then
// the app prevents the access to deep link & falls back to splash screen.
getData('studentToken').then((token) => {
if (token === '' || token === undefined) setIsLoggedIn(false);
else setIsLoggedIn(true);
});
Linking.addEventListener('url', _handleOpenUrl);
return () => {
Linking.removeEventListener('url', _handleOpenUrl);
};
}, []);
return (
//linking is enabled only if the user is logged in
<NavigationContainer linking={isLoggedIn && linking}>
<Stack.Navigator
initialRouteName="SplashScreen"
screenOptions={{...TransitionPresets.SlideFromRightIOS}}>
<Stack.Screen
name="SplashScreen"
component={SplashScreen}
options={{headerShown: false}}
/>
<Stack.Screen
name="Home"
component={Home}
options={{headerShown: false, gestureEnabled: false}}
/>
<Stack.Screen
name="Profile"
component={Profile}
options={{headerShown: false, gestureEnabled: false}}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
When a logged in user opens the deep link from notification then it will take him to the respective deep linked screen, if he's not logged in then it will open from splash screen.
I created a package for Auto Deep linking with Authentication Flow.
You can try it. auth-linking https://github.com/SohelIslamImran/auth-linking
auth-linking
Auto Deep linking with Authentication Flow
Deep linking is very easy to use with authentication. But some people take it hard way. So this package will help you to achieve the easiest way to handle Deep linking with Authentication Flow.
Installation
npm install auth-linking
yarn add auth-linking
Usage
AuthLinkingProvider
You need to wrap your app with AuthLinkingProvider.
import AuthLinkingProvider from "auth-linking";
...
const App = () => {
return (
<AuthLinkingProvider onAuthChange={onAuthChange}>
{/* Your app components */}
</AuthLinkingProvider>
);
};
export default App;
onAuthChange prop
You need to provide an onAuthChange prop to AuthLinkingProvider. This a function that should return a promise with the user or truthy value (if logged in) and null or falsy (if the user is not logged in).
const onAuthChange = () => {
return new Promise((resolve, reject) => {
onAuthStateChanged(auth, resolve, reject);
});
};
...
<AuthLinkingProvider onAuthChange={onAuthChange}>
useAutoRedirectToDeepLink
Call this hook inside a screen that will render after all auth flow is completed. So this hook will automatically redirect to the deep link through which the app is opened.
import { useAutoRedirectToDeepLink } from "auth-linking";
...
const Home = () => {
useAutoRedirectToDeepLink()
return (
<View>{...}</View>
);
};
All done.

Implementing UISplitViewController using React Navigation

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.

How to navigate in mobx store using react navigation?

I can use this.props.navigation from screen component to navigate. How should I do the similar in mobx store file? Or should I perform navigation in store?
I read the Navigating without the navigation prop article, but it seems only works for screen components, right?
Someone says use global variable to store a this.props.navigation reference and use it anywhere, but I don't like the idea...
Yes either:
forward the navigation class to the store when calling the method:
// add nivagation parameter to store fucntion:
this.props.store.handleSomething(data, this.props.navigation);
Or you can singleton the navigator (warning only works for one ofc):
return <Navigator ref={(nav) => this.props.store.navigator = nav} />;
after this is rendered it will set the navigator property in the store.
But I would suggest to store all your routing state also in a routing store like this: https://github.com/alisd23/mobx-react-router.
This way you always have easy access to the navigation state and you can be sure everything properly re-renders. (when in render function in components also depends on navigation changes!)
You can keep all your states including navigation state in mobx store.
For example:
// sourced and modified from https://github.com/react-community/react-navigation/issues/34#issuecomment-281651328
class NavigationStore {
#observable headerTitle = "Index"
#observable.ref navigationState = {
index: 0,
routes: [
{ key: "Index", routeName: "Index" },
],
}
// NOTE: the second param, is to avoid stacking and reset the nav state
#action dispatch = (action, stackNavState = true) => {
const previousNavState = stackNavState ? this.navigationState : null;
return this.navigationState = AppNavigator
.router
.getStateForAction(action, previousNavState);
}
}
// NOTE: the top level component must be a reactive component
#observer
class App extends React.Component {
constructor(props, context) {
super(props, context)
// initialize the navigation store
this.store = new NavigationStore()
}
render() {
// patch over the navigation property with the new dispatch and mobx observed state
return (
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.store.dispatch,
state: this.store.navigationState,
addListener: () => { /* left blank */ }
})}/>
)
}
};
Then you can directly call the dispatch action of the store to navigate to a new screen.
Send this one this.props.navigation as a parameter to the store. Then use as you use on the component side.
LoginStore.login(this.props.navigation)
in the LoginStore
#action login = (navigation) => { navigation.navigate('Page');}