Remove last route from react navigation stack - reactjs

So, I have the following screens:
- ChatList
- NewRoom
- ChatRoom
Basically, I don't want to go back to Start a new chat from the just-created chat room ... but instead go directly into the chat rooms list. So far, I came up with the following:
const prevGetStateForActionChatStack = ChatStack.router.getStateForAction
ChatStack.router.getStateForAction = (action, state) => {
if (state && action.type === 'RemovePreviousScreen') {
const routes = state.routes.slice( 0, state.routes.length - 2 ).concat( state.routes.slice( -1 ) )
return {
...state,
routes,
index: routes.length - 1
}
}
return prevGetStateForActionChatStack(action, state)
}
And it theoretically works ... but there is a weird animation when removing the previous route after getting to the new room, as follows. Let me know if you guys have any solution to this issue ...

In react-navigation#3.0
import { StackActions, NavigationActions } from 'react-navigation';
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'Profile' })],
});
this.props.navigation.dispatch(resetAction);
https://reactnavigation.org/docs/en/stack-actions.html#reset
In react-navigation#6.0
The reset action is replaced by replace.
import { StackActions } from '#react-navigation/native';
navigation.dispatch(
StackActions.replace('Profile', {user: 'jane',})
);
https://reactnavigation.org/docs/stack-actions/#replace

From your code it seems you are using react-navigation.
React-Navigation has a reset action that allows you to set the screen stack.
For example:
In your case,
Screen 1: Chat room
Screen 2: Chat list
If you want to remove the chatroom screen from your stack you need to write it as
import { NavigationActions } from 'react-navigation'
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'chatlist'})
]
})
this.props.navigation.dispatch(resetAction)
This will reset your stack to only one screen as initial screen that is chatlist.
actions array can have multiple routes and index defines the active route.
For further details refer the following link:
https://reactnavigation.org/docs/navigators/navigation-actions
Resetting the navigation stack for the home screen (React Navigation and React Native)

you should be able to use the following to change the animation:
export const doNotAnimateWhenGoingBack = () => ({
// NOTE https://github.com/react-community/react-navigation/issues/1865 to avoid back animation
screenInterpolator: sceneProps => {
if (Platform.isIos) {
// on ios the animation actually looks good! :D
return CardStackStyleInterpolator.forHorizontal(sceneProps);
}
if (
sceneProps.index === 0 &&
sceneProps.scene.route.routeName !== 'nameOfScreenYouWannaGoTo' &&
sceneProps.scenes.length > 2
)
return null;
return CardStackStyleInterpolator.forVertical(sceneProps);
},
});
and use it as follows:
const Stack = StackNavigator(
{
...screens...
},
{
transitionConfig: doNotAnimateWhenGoingBack,
}
);

Related

How to detect the device on React SSR App with Next.js?

on a web application I want to display two different Menu, one for the Mobile, one for the Desktop browser.
I use Next.js application with server-side rendering and the library react-device-detect.
Here is the CodeSandox link.
import Link from "next/link";
import { BrowserView, MobileView } from "react-device-detect";
export default () => (
<div>
Hello World.{" "}
<Link href="/about">
<a>About</a>
</Link>
<BrowserView>
<h1> This is rendered only in browser </h1>
</BrowserView>
<MobileView>
<h1> This is rendered only on mobile </h1>
</MobileView>
</div>
);
If you open this in a browser and switch to mobile view and look the console you get this error:
Warning: Text content did not match. Server: " This is rendered only
in browser " Client: " This is rendered only on mobile "
This happen because the rendering by the server detects a browser and on the client, he is a mobile device. The only workaround I found is to generate both and use the CSS like this:
.activeOnMobile {
#media screen and (min-width: 800px) {
display: none;
}
}
.activeOnDesktop {
#media screen and (max-width: 800px) {
display: none;
}
}
Instead of the library but I don't really like this method. Does someone know the good practice to handle devices type on an SSR app directly in the react code?
LATEST UPDATE:
So if you don't mind doing it client side you can use the dynamic importing as suggested by a few people below. This will be for use cases where you use static page generation.
i created a component which passes all the react-device-detect exports as props (it would be wise to filter out only the needed exports because then does not treeshake)
// Device/Device.tsx
import { ReactNode } from 'react'
import * as rdd from 'react-device-detect'
interface DeviceProps {
children: (props: typeof rdd) => ReactNode
}
export default function Device(props: DeviceProps) {
return <div className="device-layout-component">{props.children(rdd)}</div>
}
// Device/index.ts
import dynamic from 'next/dynamic'
const Device = dynamic(() => import('./Device'), { ssr: false })
export default Device
and then when you want to make use of the component you can just do
const Example = () => {
return (
<Device>
{({ isMobile }) => {
if (isMobile) return <div>My Mobile View</div>
return <div>My Desktop View</div>
}}
</Device>
)
}
Personally I just use a hook to do this, although the initial props method is better.
import { useEffect } from 'react'
const getMobileDetect = (userAgent: NavigatorID['userAgent']) => {
const isAndroid = () => Boolean(userAgent.match(/Android/i))
const isIos = () => Boolean(userAgent.match(/iPhone|iPad|iPod/i))
const isOpera = () => Boolean(userAgent.match(/Opera Mini/i))
const isWindows = () => Boolean(userAgent.match(/IEMobile/i))
const isSSR = () => Boolean(userAgent.match(/SSR/i))
const isMobile = () => Boolean(isAndroid() || isIos() || isOpera() || isWindows())
const isDesktop = () => Boolean(!isMobile() && !isSSR())
return {
isMobile,
isDesktop,
isAndroid,
isIos,
isSSR,
}
}
const useMobileDetect = () => {
useEffect(() => {}, [])
const userAgent = typeof navigator === 'undefined' ? 'SSR' : navigator.userAgent
return getMobileDetect(userAgent)
}
export default useMobileDetect
I had the problem that scroll animation was annoying on mobile devices so I made a device based enabled scroll animation component;
import React, { ReactNode } from 'react'
import ScrollAnimation, { ScrollAnimationProps } from 'react-animate-on-scroll'
import useMobileDetect from 'src/utils/useMobileDetect'
interface DeviceScrollAnimation extends ScrollAnimationProps {
device: 'mobile' | 'desktop'
children: ReactNode
}
export default function DeviceScrollAnimation({ device, animateIn, animateOut, initiallyVisible, ...props }: DeviceScrollAnimation) {
const currentDevice = useMobileDetect()
const flag = device === 'mobile' ? currentDevice.isMobile() : device === 'desktop' ? currentDevice.isDesktop() : true
return (
<ScrollAnimation
animateIn={flag ? animateIn : 'none'}
animateOut={flag ? animateOut : 'none'}
initiallyVisible={flag ? initiallyVisible : true}
{...props}
/>
)
}
UPDATE:
so after further going down the rabbit hole, the best solution i came up with is using the react-device-detect in a useEffect, if you further inspect the device detect you will notice that it exports const's that are set via the ua-parser-js lib
export const UA = new UAParser();
export const browser = UA.getBrowser();
export const cpu = UA.getCPU();
export const device = UA.getDevice();
export const engine = UA.getEngine();
export const os = UA.getOS();
export const ua = UA.getUA();
export const setUA = (uaStr) => UA.setUA(uaStr);
This results in the initial device being the server which causes false detection.
I forked the repo and created and added a ssr-selector which requires you to pass in a user-agent. which could be done using the initial props
UPDATE:
Because of Ipads not giving a correct or rather well enough defined user-agent, see this issue, I decided to create a hook to better detect the device
import { useEffect, useState } from 'react'
function isTouchDevice() {
if (typeof window === 'undefined') return false
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
function mq(query) {
return typeof window !== 'undefined' && window.matchMedia(query).matches
}
// #ts-ignore
if ('ontouchstart' in window || (window?.DocumentTouch && document instanceof DocumentTouch)) return true
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('') // include the 'heartz' - https://git.io/vznFH
return mq(query)
}
export default function useIsTouchDevice() {
const [isTouch, setIsTouch] = useState(false)
useEffect(() => {
const { isAndroid, isIPad13, isIPhone13, isWinPhone, isMobileSafari, isTablet } = require('react-device-detect')
setIsTouch(isTouch || isAndroid || isIPad13 || isIPhone13 || isWinPhone || isMobileSafari || isTablet || isTouchDevice())
}, [])
return isTouch
Because I require the package each time I call that hook, the UA info is updated, it also fixes to SSR out of sync warnings.
I think you should do it by using getInitialProps in your page, as it runs both on the server and on the client, and getting the device type by first detecting if you are just getting the request for the webpage (so you are still on the server), or if you are re-rendering (so you are on the client).
// index.js
IndexPage.getInitialProps = ({ req }) => {
let userAgent;
if (req) { // if you are on the server and you get a 'req' property from your context
userAgent = req.headers['user-agent'] // get the user-agent from the headers
} else {
userAgent = navigator.userAgent // if you are on the client you can access the navigator from the window object
}
}
Now you can use a regex to see if the device is a mobile or a desktop.
// still in getInitialProps
let isMobile = Boolean(userAgent.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
))
return { isMobile }
Now you can access the isMobile prop that will return either true or false
const IndexPage = ({ isMobile }) => {
return (
<div>
{isMobile ? (<h1>I am on mobile!</h1>) : (<h1>I am on desktop! </h1>)}
</div>
)
}
I got this answer from this article here
I hope that was helpful to you
UPDATE
Since Next 9.5.0, getInitialProps is going to be replaced by getStaticProps and getServerSideProps. While getStaticProps is for fetching static data, which will be used to create an html page at build time, getServerSideProps generates the page dynamically on each request, and receives the context object with the req prop just like getInitialProps. The difference is that getServerSideProps is not going to know navigator, because it is only server side. The usage is also a little bit different, as you have to export an async function, and not declare a method on the component. It would work this way:
const HomePage = ({ deviceType }) => {
let componentToRender
if (deviceType === 'mobile') {
componentToRender = <MobileComponent />
} else {
componentToRender = <DesktopComponent />
}
return componentToRender
}
export async function getServerSideProps(context) {
const UA = context.req.headers['user-agent'];
const isMobile = Boolean(UA.match(
/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i
))
return {
props: {
deviceType: isMobile ? 'mobile' : 'desktop'
}
}
}
export default HomePage
Please note that since getServerSideProps and getStaticProps are mutually exclusive, you would need to give up the SSG advantages given by getStaticProps in order to know the device type of the user. I would suggest not to use getServerSideProps for this purpose if you need just to handle a couple of styiling details. If the structure of the page is much different depending on the device type than maybe it is worth it
Load only the JS files needed dynamically
You can load components dynamically with next/dynamic, and only the appropriate component will be loaded.
You can use react-detect-device or is-mobile and in my case. In this scenario, I created separate layout for mobile and desktop, and load the appropriate component base on device.
import dynamic from 'next/dynamic';
const mobile = require('is-mobile');
const ShowMobile = dynamic(() => mobile() ? import('./ShowMobile.mobile') : import('./ShowMobile'), { ssr: false })
const TestPage = () => {
return <ShowMobile />
}
export default TestPage
You can view the codesandbox . Only the required component.JS will be loaded.
Edit:
How different is the above from conditionally loading component? e.g.
isMobile ? <MobileComponent /> : <NonMobileComponent />
The first solution will not load the JS file, while in second solution, both JS files will be loaded. So you save one round trip.
With current Next.js (v 9.5+) I accomplished that using next/dynamic and react-detect-device.
For instance, on my header component:
...
import dynamic from 'next/dynamic';
...
const MobileMenuHandler = dynamic(() => import('./mobileMenuHandler'), {
ssr: false,
});
return (
...
<MobileMenuHandler
isMobileMenuOpen={isMobileMenuOpen}
setIsMobileMenuOpen={setIsMobileMenuOpen}
/>
)
...
Then on MobileMenuHandler, which is only called on the client:
import { isMobile } from 'react-device-detect';
...
return(
{isMobile && !isMobileMenuOpen ? (
<Menu
onClick={() => setIsMobileMenuOpen(true)}
className={classes.menuIcon}
/>
) : null}
)
With that, the react-detect-device is only active on the client side and can give a proper reading.
See Next.js docs.
When I was working on one of my next.js projects, I came across a similar situation. I have got some ideas from the answers. And I did solve it with the following approach.
Firstly, I made custom hook using react-device-detect
//hooks/useDevice.ts
import { isDesktop, isMobile } from 'react-device-detect';
interface DeviceDetection {
isMobile: boolean;
isDesktop: boolean;
}
const useDevice = (): DeviceDetection => ({
isMobile,
isDesktop
});
export default useDevice;
Secondly, I made a component which uses of custom hook
//Device/Device.tsx
import { ReactElement } from 'react';
import useDevice from '#/hooks/useDevice';
export interface DeviceProps {
desktop?: boolean;
mobile?: boolean;
children: ReactElement;
}
export const Device = ({ desktop, mobile, children }: DeviceProps): ReactElement | null => {
const { isMobile } = useDevice();
return (isMobile && mobile) || (!isMobile && desktop) ? children : null;
};
Thirdly, I import the component dynamically using next.js next/dynamic
//Device/index.tsx
import dynamic from 'next/dynamic';
import type { DeviceProps } from './Device';
export const Device = dynamic<DeviceProps>(() => import('./Device').then((mod) => mod.Device), {
ssr: false
});
Finally, I used it following way in pages.
//pages/my-page.tsx
import { Device } from '#/components/Device';
<Device desktop>
<my-component>Desktop</my-component>
</Device>
<Device mobile>
<my-component>Mobile</my-component>
</Device>
There is a way to resolve with react-device-detect.
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
const userAgent = req.headers['user-agent'] || '';
const { isMobile } = getSelectorsByUserAgent(userAgent);
return {
props: { isMobile },
};
}
you can find more keys below because it is not specified on type definition of react-device-detect lib.
{
isSmartTV: false,
isConsole: false,
isWearable: false,
isEmbedded: false,
isMobileSafari: false,
isChromium: false,
isMobile: false,
isMobileOnly: false,
isTablet: false,
isBrowser: true,
isDesktop: true,
isAndroid: false,
isWinPhone: false,
isIOS: false,
isChrome: true,
isFirefox: false,
isSafari: false,
isOpera: false,
isIE: false,
osVersion: '10.15.7',
osName: 'Mac OS',
fullBrowserVersion: '107.0.0.0',
browserVersion: '107',
browserName: 'Chrome',
mobileVendor: 'none',
mobileModel: 'none',
engineName: 'Blink',
engineVersion: '107.0.0.0',
getUA: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36',
isEdge: false,
isYandex: false,
deviceType: 'browser',
isIOS13: false,
isIPad13: false,
isIPhone13: false,
isIPod13: false,
isElectron: false,
isEdgeChromium: false,
isLegacyEdge: false,
isWindows: false,
isMacOs: true,
isMIUI: false,
isSamsungBrowser: false
}
Was able to avoid dynamic importing or component props, by using React state instead. For my use case, I was trying to detect if it was Safari, but this can work for other ones as well.
Import code
import { browserName } from 'react-device-detect';
Component code
const [isSafari, setIsSafari] = useState(false);
useEffect(() => {
setIsSafari(browserName === 'Safari');
}, [browserName]);
// Then respect the state in the render
return <div data-is-safari={isSafari} />;
If you don't mind rendering always desktop version and figuring the logic on the front-end, then the hook logic can be pretty straightforward.
export const useDevice = () => {
const [firstLoad, setFirstLoad] = React.useState(true);
React.useEffect(() => { setFirstLoad(false); }, []);
const ssr = firstLoad || typeof navigator === "undefined";
const isAndroid = !ssr && /android/i.test(navigator.userAgent);
const isIos = !ssr && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return {
isAndroid,
isIos,
isDesktop: !isAndroid && !isIos
};
};
import React, { useState, useEffect }
import { isMobile } from 'react-device-detect'
...
const [_isMobile, setMobile] = useState();
useEffect(() => {
setMobile(isMobile);
}, [setMobile]);
<div hidden={_isMobile}> Desktop View</div>
<div hidden={!_isMobile}> MobileView </div>
I solved a case like this using next-useragent.
const mobileBreakpoint = 1280;
/**
*
* #param userAgent - the UserAgent object from `next-useragent`
*/
export const useIsMobile = (userAgent?: UserAgent): boolean => {
const [isMobile, setIsMobile] = useState(false);
// Some front-end hook that gets the current breakpoint, but returns undefined, if we don't have a window object.
const { breakpoint } = useResponsive();
useEffect(() => {
if (breakpoint) {
setIsMobile(breakpoint.start < mobileBreakpoint);
}
else if (userAgent) {
setIsMobile(userAgent.isMobile);
} else if (!isBrowser) {
setIsMobile(false);
}
}, [userAgent, breakpoint]);
return isMobile;
};
And the usage of it is:
// Inside react function component.
const isMobile = useIsMobile(props.userAgent);
export const getServerSideProps = (
context: GetServerSidePropsContext,
): GetServerSidePropsResult<{ userAgent?: UserAgent }> => ({
// Add the user agent to the props, so we can use it in the window hook.
props: {
userAgent: parse(context.req.headers["user-agent"] ?? ""),
},
});
This hook always returns a boolean isMobile. When you run it server-side, it uses the user-agent header to detect a mobile device in the SSR request. When this gets to client side, it uses the breakpoints (in my case), or any other logic for width detection to update the boolean. You could use next-useragent to also detect the specific device type, but you can't make resolution-based rendering server-side.
If you want to do something with user-agent information in nextjs from server side you'll have to use getServerSide props. because this is the only function that has access to req object. getStaticProps is not helpful.
First create a helper function just to reuse on several pages.
const getDevice = (userAgent) => {
let device = "";
if(userAgent && userAgent !== ""){
let isMobile = userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i)
if(isMobile && isMobile?.length > 0){
device = "mobile";
}
}
return device
}
You can further modify above function as per your need.
Now in your getServerSideProps:
export const getServerSideProps = ({req}) => {
const device = getDevice(req.headers['user-agent']);
return {
props: {
device,
}
}
}
Now you have device information in your page. You can use to render different totally different layouts just like flipkart and olx.
NOTE : Changes will only reflect when a fresh page will be requested because server does not aware of client changes in viewport. If you want such thing probably you can use context api.
The downside is : You have to make each page that shifts layout, a server rendered page.
However if you are going to deploy your nextjs on netlify consider using middlewares with combination of #netlify/next package. More info here
This always works. (I used this package after trying the above technique and it didn't work for me.)
The advantage: The component renders server side so there's no flashing on client side when trying to detect user agent.
import { isMobile } from "mobile-device-detect";
just import the package and create your layout.
import { isMobile } from "mobile-device-detect";
const Desktop = () => {
return (
<>
desktop
</>
);
};
Desktop.layout = Layout;
const Mobile = () => {
return (
<>
mobile
</>
);
};
Mobile.layout = LayoutMobile;
const Page = isMobile ? Desktop : Mobile;
export default Page;

How To Handle conditional Routing or Component Navigation Without React Router

I need to navigate between components based on several conditions, and I do not want routes to be displayed in the browser, say localhost:3000/step1 or localhost:3000/step2. The whole application is guided so that a user have to answer all the steps to reach the final result.
ATM I have a main container which handles the component rendering based on the Redux store value.
import React, { Component } from "react";
class Home extends Component {
renderComponent = screen => {
switch (screen) {
case 'SCREEN_A':
return <ScreenA />;
case 'SCREEN_B':
return <ScreenB />;
case 'SCREEN_C':
return <ScreenC />;
case 'SCREEN_D':
return <ScreenD />;
default:
return <ScreenA />;
}
};
render() {
return <div>{this.renderComponent(this.props.currentScreen)}</div>;
}
}
function mapStateToProps(storeData) {
return {
store: storeData,
currentScreen: storeData.appState.currentScreen,
userData: storeData.userData
};
}
export default connect(mapStateToProps)(Home);
The problem is I have to use dispatch to trigger navigation
navigateTo(screens[destination])
navigateBackward(currentScreen)
navigateForward(currentScreen)
in almost all components. I do have a predefined JSON for each component which contains the destination for each screen.
screens : {
SCREEN_A:{
id: 1,
name: 'SCREEN_A',
next: 'SCREEN_B',
back: 'WELCOME_SCREEN',
activeLoader: true,
},
SCREEN_B:{
id: 2,
name: 'SCREEN_B',
next: 'SCREEN_C',
back: 'WELCOME_SCREEN',
activeLoader: true,
},
SCREEN_C:{
id: 3,
name: 'SCREEN_C',
next: 'SCREEN_D',
back: 'SCREEN_A',
activeLoader: true,
},
SCREEN_D:{
id: 4,
name: 'SCREEN_D',
next: 'SCREEN_E',
back: 'SCREEN_D',
activeLoader: true,
},
}
And there are protected screens which makes things way more complicated. Is there a better way of doing this with redux? or should I create a middleware and intercept each state change and calculate the next screen.
I would change a few things:
Make your steps/screens dynamic. By putting them into an Array and using the index to determine the current step it removes a lot of code and will make it easier to add/move steps.
Store the steps/screens config in the redux store.
Optionally, you can pass the nextStep and previousStep to the StepComponent. e.g. <StepComponent nextStep={nextStep} previousStep={previousStep} />.
In your last step, you probably want to call a different action instead of nextStep.
Here's what my solution would look like:
// Home.jsx
import React, { Component } from 'react';
import * as types from '../../redux/Actor/Actor.types';
class Home extends Component {
stepComponents = [
ScreenA,
ScreenB,
ScreenC,
ScreenD,
];
render() {
const { step, steps } = this.props;
const StepComponent = this.stepComponents[step];
return (
<div>
<StepComponent {...steps[step]} />
</div>
);
}
}
// store.jsx
export default {
step : 0,
steps: [
{
id : 1,
name : 'SCREEN_A',
activeLoader: true,
},
....
],
};
// actions.jsx
export const nextStep = () => ({ type: 'NEXT_STEP' });
export const previousStep = () => ({ type: 'PREVIOUS_STEP' });
// reducers.jsx
export const nextStep = state => ({ ...state, step: state.step + 1 });
export const previousStep = state => ({ ...state, step: state.step - 1 });

Navigate to parent of parent stack

I have this scenario where i have a StackNavigator nested in a TabNavigator nested in another StackNavigator.
const TabOneStack = StackNavigator({
ScreenA: { screen: ScreenA },
ScreenB: { screen: ScreenB }
});
const MainTabs = TabNavigator({
TabOne: { screen: TabOneStack },
TabTwo: { screen: TabTwoStack }
});
const Root = StackNavigator({
HomeScreen: { screen: HomeScreen },
MainTabs: { screen: MainTabs }
});
Everything works but i cant figure out how to navigate for example from ScreenA back to the Home screen in the root StackNavigator.
After the HomeScreen the User navigates directly to ScreenA.
The back button in the header in ScreenA works fine and brings me back to Home but need a way to have a button that brings me back to the HomeScreen.
this.props.navigation.goBack() does not work unfortunately.
i also tried
const backAction = NavigationActions.reset({
index: 0,
key: null,
actions: [
NavigationActions.navigate({ routeName: 'HomeScreen'})
]
});
this.props.navigation.dispatch(backAction));
but i get:
There is no route defined for key HomeScreen. Must be one of: 'ScreenA', 'ScreenB'.
What is the correct way to do this?
To traverse from child StackNavigator back to parent StackNavigator, do:
class ScreenA extends Component {
render() {
return (<Button onPress={() => {
this.props.navigation.dispatch({type: 'Navigation/BACK'});
}} title='ScreenA. back to Home' />);
}
}
this.props.navigation.dispatch() with 'Navigation/BACK' works exactly the same as the top-most back button.
It differs from goBack() by apply parent-child stack traversing, while goBack() does not.
Result:
And note #Jigar's answer is also correct, it's just a shorthand notation of mine. The key is to pass null argument into goBack(). It won't work without that.
this.props.navigation.goBack(null);
In the new react native version we use 'StackActions' for this case:
StackActions reference
the 'key' parameter in this case is the key to solve this problem. Usually we manage the stack like this:
const resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'ScreenA' })],
});
this.props.navigation.dispatch(resetAction);
but StackActions object has another option and its: 'key' - its an optional string or null - If set, the navigator with the given key will reset. If null, the root navigator will reset.
So in this case, to reset back to root and navigate to some screen inside it -> in your case from 'ScreenA' to 'HomeScreen' - you should write:
this.props.navigation.dispatch(
StackActions.reset({
index: 0,
key:null,
actions: [
NavigationActions.navigate({
routeName: 'HomeScreen'
})
]
})
);
Notice, in this new version we no longer use 'NavigationActions' to reset the stack, only 'StackActions'.
use this
this.props.navigation.goBack(null);
In v6 you can use something like this:
navigation.getParent()?.navigate('Home')
For me, it's should working fine by using this:
Change this:
this.props.navigation.dispatch(backAction));
to
this.navigator.dispatch(backAction);
Also in your render
render() {
return (
<Root
ref={(nav) => {
this.navigator = nav;
}}
/>
);
}

React Navigation redux navigate

I'd like to know how the navigation supposed to happen, when redux stores the navigation state.
Short version:
If the redux store isn't in initial state, a screen is mounted without actually navigationg there.
Detailed description:
Now I'm able to navigate either by (1) using the navigation props given by the parent navigator (StackNavigator in my case) or by (2) dispatching an action.
1: this.props.navigation.navigate('main')
2: this.props.navigateToMainAction()
The reducer:
const INIT_STATE = Nav.router.getStateForAction(
NavigationActions.navigate({ routeName: 'login' })
);
const navReducer = (state = INIT_STATE, action) => {
const newState = Nav.router.getStateForAction(action, state);
return newState || state;
};
Nav structure:
const authStack = StackNavigator({
login: { screen: LoginScreen }
,forgottendPassword: { screen: LoginScreen }
}, {
initialRouteName: 'login'
,headerMode: 'none'
});
const homeDrawer = DrawerNavigator({
home: {
screen: HomeScreen
,navigationOptions: { drawerLockMode: 'locked-closed' }
}
}, {
drawerPosition: 'right'
,drawerWidth: 300
,contentComponent: props => <HomeDrawerMenu {...props} />
});
const mainStack = StackNavigator({
homeDrawer: {
screen: homeDrawer
,navigationOptions: ({ navigation }) => ({
header: <HomeMenu navigate={navigation.navigate} />
})
}
,partnerList: {
screen: PartnerListScreen
,navigationOptions: ({ navigation }) => ({
header: <PartnerListMenu navigation={navigation} />
})
}
}, {
initialRouteName: 'homeDrawer'
});
const Nav = StackNavigator({
auth: { screen: authStack }
,main: { screen: mainStack }
}, {
initialRouteName: 'auth',
headerMode: 'none',
});
When I try to navigate from auth/login to main and from there to main/partnerList it only works correctly, if the redux store is in initial state (after I cleared the async storage).
But when I reload the app, it mounts the main/partnerList component, without actually navigating there and thanks to that, the action which fetching the partnerList comp.'s data is also called.
Expected action order on load:
##INIT
Offline/STATUS_CHANGED
persist/REHYDRATE
check_token <- Check if the user is logged in (in this case he is)
check_token_commit
Navigation/NAVIGATE <- The user is forwarded to main, when he is logged in
Navigation/NAVIGATE <- The user goes to the partnerList component
fetch_partner_list <- Action for fetching the partner list
Offline/BUSY
fetch_partner_list_commit
Current action order on load:
##INIT
Offline/STATUS_CHANGED
persist/REHYDRATE
check_token <- Check if the user is logged in (in this case he is)
fetch_partner_list <- It's already fetching, but no navigate action was triggered yet
Offline/BUSY
check_token_commit
Navigation/NAVIGATE <- The user is forwarded to main, when he is logged in
Navigation/NAVIGATE <- The forwarding happens twice, for some reason
fetch_partner_list_commit
Navigation/NAVIGATE <- The user goes to the partnerList component
fetch_partner_list <- The only time it should be triggered
Offline/BUSY
fetch_partner_list_commit
The problem was coused by the inappropriate use of the react navigation.
I had to wrap the auth and main router and pass down in screenProps the root navigation object.

Status of React Navigation drawer? (open or closed)

I'm building a drawer with React Navigation and want to perform some logic if the user closes the drawer. I don't see anything obvious in the documentation that will allow me to do this. Is anyone aware of a way to do this?
You need to custom navigation actions to capture the DrawerClose event:
const MyDrawerNavigator = DrawerNavigator({
//...
});
const defaultGetStateForAction = MyDrawerNavigator.router.getStateForAction;
MyDrawerNavigator.router.getStateForAction = (action, state) => {
//use 'DrawerOpen' to capture drawer open event
if (state && action.type === 'Navigation/NAVIGATE' && action.routeName === 'DrawerClose') {
console.log('DrawerClose');
//write the code you want to deal with 'DrawerClose' event
}
return defaultGetStateForAction(action, state);
};
According to #ufxmeng
import {
StatusBar,
} from "react-native";
const MyDrawerNavigator = DrawerNavigator({
//...
});
const defaultGetStateForAction = MyDrawerNavigator.router.getStateForAction;
MyDrawerNavigator.router.getStateForAction = (action, state) => {
if(state && action.type === 'Navigation/NAVIGATE' && action.routeName === 'DrawerClose') {
StatusBar.setHidden(false);
}
if(state && action.type === 'Navigation/NAVIGATE' && action.routeName === 'DrawerOpen') {
StatusBar.setHidden(true);
}
return defaultGetStateForAction(action, state);
};
See here https://github.com/react-community/react-navigation/blob/673b9d2877d7e84fbfbe2928305ead7e51b04835/docs/api/routers/Routers.md
and here https://github.com/aksonov/react-native-router-flux/issues/699
This is for anyone using the v2.0+ version of react-navigation. There are now drawer actions you can track.
{
OPEN_DRAWER: 'Navigation/OPEN_DRAWER',
CLOSE_DRAWER: 'Navigation/CLOSE_DRAWER',
TOGGLE_DRAWER: 'Navigation/TOGGLE_DRAWER',
DRAWER_OPENED: 'Navigation/DRAWER_OPENED',
DRAWER_CLOSED: 'Navigation/DRAWER_CLOSED'
}
react-navigation.js line 825
However, it seems that implied drawer navigations from say swiping don't fire the OPEN_/CLOSE_ actions, since you didn't manually toggle it. The _OPENED/_CLOSED actions do fire afterwards, though.
const MyDrawerNavigator = createDrawerNavigator(...);
const defaultGetStateForAction = MyDrawerNavigator.router.getStateForAction;
MyDrawerNavigator.router.getStateForAction = (action, state) => {
switch (action.type) {
case 'Navigation/OPEN_DRAWER':
case 'Navigation/DRAWER_OPENED':
StatusBar.setHidden(true, 'slide');
break;
case 'Navigation/CLOSE_DRAWER':
case 'Navigation/DRAWER_CLOSED':
StatusBar.setHidden(false, 'slide');
break;
}
return defaultGetStateForAction(action, state);
};
Working with "react-navigation": "^3.5.1"
const defaultGetStateForAction = DrawerNav.router.getStateForAction;
DrawerNav.router.getStateForAction = (action, state) => {
if(action){
if(action.type == 'Navigation/MARK_DRAWER_SETTLING' && action.willShow){
StatusBar.setHidden(true);
} else if(action.type == 'Navigation/MARK_DRAWER_SETTLING' && !action.willShow) {
StatusBar.setHidden(false);
}
}
return defaultGetStateForAction(action, state);
};
For anyone looking to wire it up such that the drawer events are available in one of your Screens or Components instead of the top level app, I was able to wire that up by using screenProps as described in this post. You first set up the screenProps on your app and pass in the router and whatever else you need. You can pull screenProps off the props and use it your screen or component (I wired it up in the constructor in this example), use getStateForAction to setState in your component driven off the router events.
Here is an example (some code removed for clarity)
App.js
import React from 'react';
import { AppLoading } from 'expo';
import {
createDrawerNavigator,
createAppContainer,
createStackNavigator,
} from 'react-navigation';
import { HomeScreen } from './src/screens/HomeScreen.android';
import { LanguageSelectScreen } from './src/screens/LanguageSelectScreen';
export default class App extends React.Component {
state = {
isLoadingComplete: false,
isTilted: false,
};
constructor() {
super();
}
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading />
);
} else {
return (
<MyApp screenProps={MyAppNavigator.router} />
);
}
}
}
const MyAppNavigator = createDrawerNavigator(
{
Home: {
screen: HomeScreen,
},
PlayerNameScreen: {
screen: PlayerNameScreen,
},
},
{
unmountInactiveRoutes: true,
initialRouteName: 'PlayerNameScreen',
},
);
const RootStack = createStackNavigator(
{
Main: {
screen: MyAppNavigator,
},
MyModal: {
screen: LanguageSelectScreen,
},
},
{
mode: 'modal',
headerMode: 'none',
},
);
export const MyApp = createAppContainer(RootStack);
HomeScreen.android.js
import React from 'react';
import {Icon} from 'react-native-elements';
export class HomeScreen extends React.Component {
static navigationOptions = {
drawerLabel: () => 'Home',
drawerIcon: ({ tintColor }) => (
<Icon
name="checkerboard"
type="material-community"
size={25}
color={tintColor}
/>
),
};
constructor(props) {
super(props);
const router = props.screenProps;
const defaultGetStateForAction = router.getStateForAction;
router.getStateForAction = (action, state) => {
switch (action.type) {
case 'Navigation/MARK_DRAWER_SETTLING':
if (action.willShow == false) {
console.log('CLOSED');
this.setState({ isTilted: false });
} else if (action.willShow == true) {
this.setState({ isTilted: true });
console.log('OPEN');
}
break;
}
return defaultGetStateForAction(action, state);
};
this.state = {
isTilted: false,
};
}
render() {
const { isTilted } = this.state;
// ... render using isTilted
}
}
Now in react navigation 5 you can directly access the status of your drawer using this approach :
useIsDrawerOpen() is a Hook to detect if the drawer is open in a parent navigator.
For exemple in your view you can test if your drawer is open or not directly using this approach :
import { useIsDrawerOpen } from '#react-navigation/drawer';
const MainContainer = () => {
return (
<View style={(useIsDrawerOpen()) ? styles.conatinerOpenStyle : styles.containerClosedStyle}>
<Text>{console.log("Test drawer "+useIsDrawerOpen())}</Text>
</View>
);}
I found something similar to asdfghjklm.
Here's my code which is working perfectly, even when you use a swipe to open - and then use a button to close it:
openCloseDrawer = (props) => {
if (props.navigation.state.index == 1) {
props.navigation.navigate('DrawerClose');
} else {
props.navigation.navigate('DrawerOpen');
}
}
I execute this function when the user taps on the "Open/Close Drawer" button.
Based on Brad Bumbalough, fredrivett (thank you mans) solution I find a more fast response solution (the other solution delays secons in some uses).
const defaultGetStateForAction = StackLogadoDrawer.router.getStateForAction;
StackLogadoDrawer.router.getStateForAction = (action, state) => {
switch (action.type) {
case 'Navigation/MARK_DRAWER_SETTLING':
if (action.willShow == false) {
console.log('CERRADO');
} else if (action.willShow == true) {
console.log('ABIERTO');
}
break;
}
return defaultGetStateForAction(action, state);
};
This is fired just immediately actions occurs (or very near).
It works on gestures or calling openDrawer()
Anyway I think this is a "must have" easy and direct way in the API
Hmm not really seeing much. One way would be to control the opening/closing of the drawer manually using:
this.props.navigation.navigate('DrawerOpen'); // open drawer
this.props.navigation.navigate('DrawerClose'); // close drawer
That way you can wrap your close/open events in a function where you can do whatever you want before opening/closing.
The only other possible solution I saw in their docs was using a custom contentComponent. You can pass a function to the onItemPress(route) event, so maybe you can try hooking into that.
This is working fine in React-Native, not sure about React.
For my case I had to change the StatusBar so I did not need to know wether the Drawer is closed fully or not, So below code worked for me...
props.navigation.state.drawerMovementDirection === 'closing'
? //it is closing not fully closed
: (props.navigation.state.drawerMovementDirection === 'opening' ||
props.navigation.state.isDrawerOpen) && (
//fully opened or opening
)
If you don't need to know immediate then you can use below code, and this will work fine and will give you accurate answer whether the Drawer is Open or Not!
Note: This will be delayed answer, in my case it was taking 1 sec.
props.navigation.state.isDrawerOpen?
//open
:
//close;
If the solution does't work I am very sorry, but above all answer did not worked! So this is the version which is working for me:))
useEffect(() => {
const _unsubscribe = props.navigation.addListener('state', (data) => {
var array = data.data.state.routes[0].state.history;
const mydata = array.some(item => item.hasOwnProperty('status'))
console.log('data--->', mydata)
if (mydata) {
console.log('daawer close ')
} else {
console.log('daawer open ')
}
});
return _unsubscribe;
}, [])
Without Redux integration can be used onNavigationStateChange on router component. Just intercept drawer actions: DrawerOpen and DrawerClose.
Example:
handleNavigationState = (previous, next, action) => {
if (action.routeName === 'DrawerOpen') {
this.props.setDrawerState(true);
} else if (action.routeName === 'DrawerClose') {
this.props.setDrawerState(false);
}
}
render() {
return (
<Router onNavigationStateChange={this.handleNavigationState} />
);
}
I know this is late, but for anyone who is looking for an answer:
The logic for the drawer being open/closed is in:
this.props.navigation.state.routes[0].index
It's 0 for closed, 1 for open.
You can also toggle the Drawers with
this.props.navigation.navigate('DrawerToggle')
instead of
this.props.navigation.navigate('DrawerOpen');
or
this.props.navigation.navigate('DrawerClose');
It seems to be more convenient for me and I have not happened upon any problems yet. Although it's nice to know whether they are toggled or not in order to invoke other actions.
I truly believe React-Navigation has one of the worst documentation that I have ever seen. There are commands that nobody knows about. I could not find the DrawerToggle action in the documents, and I only happened upon it through using console.log(this.props.navigation);

Resources