How to intercept back button in a React SPA using function components and React Router v5 - reactjs

I'm working in a SPA in React that doesn't use React Router to create any Routes; I don't need to allow users to navigate to specific pages. (Think multi-page questionnaire, to be filled in sequentially.)
But, when users press the back button on the browser, I don't want them to exit the whole app; I want to be able to fire a function when the user presses the back button that simply renders the page as it was before their last selection. (Each page is a component, and they're assembled in an array, tracked by a currentPage state variable (from a state hook), so I can simply render the pages[currentPage -1].
Using (if necessary) the current version of React Router (v5), function components, and Typescript, how can I access the back-button event to both disable it and replace it with my own function?
(Other answers I've found either use class components, functions specific to old versions of React Router, or specific frameworks, like Next.js.)
Any and all insight is appreciated.

After way too many hours of work, I found a solution. As it ultimately was not that difficult once I found the proper path, I'm posting my solution in the hope it may save others time.
Install React Router for web, and types - npm install --save react-router-dom #types/react-router-dom.
Import { BrowserRouter, Route, RouteComponentProps, withRouter } from react-router-dom.
Identify the component whose state will change when the back button is pressed.
Pass in history from RouteComponentProps via destructuring:
function MyComponent( { history }: ReactComponentProps) {
...
}
On screen state change (what the user would perceive as a new page) add a blank entry to history; for example:
function MyComponent( { history }: ReactComponentProps) {
const handleClick() {
history.push('')
}
}
This creates a blank entry in history, so history has entries that the back button can access; but the url the user sees won't change.
Handle the changes that should happen when the back-button on the browser is pressed. (Note that component lifecycle methods, like componentDidMount, won't work in a function component. Make sure useEffect is imported from react.)
useEffect(() => {
// code here would fire when the page loads, equivalent to `componentDidMount`.
return () => {
// code after the return is equivalent to `componentWillUnmount`
if (history.action === "POP") {
// handle any state changes necessary to set the screen display back one page.
}
}
})
Wrap it in withRouter and create a new component with access to a Route's properties:
const ComponentWithHistory = withRouter(MyComponent);
Now wrap it all in a <BrowserRouter /> and a <Route /> so that React Router recognizes it as a router, and route all paths to path="/", which will catch all routes unless Routes with more specific paths are specified (which will all be the same anyway, with this setup, due to history.push(""); the history will look like ["", "", "", "", ""]).
function App() {
return (
<BrowserRouter>
<Route path="/">
<ThemeProvider theme={theme}>
<ComponentWithHistory />
</ThemeProvider>
</Route>
</BrowserRouter>
);
}
export default App;
A full example now looks something like this:
function MyComponent( { history }: ReactComponentProps) {
// use a state hook to manage a "page" state variable
const [page, setPage] = React.useState(0)
const handleClick() {
setPage(page + 1);
history.push('');
}
useEffect(() => {
return () => {
if (history.action === "POP") {
// set state back one to render previous "page" (state)
setPage(page - 1)
}
}
})
}
const ComponentWithHistory = withRouter(MyComponent);
function App() {
return (
<BrowserRouter>
<Route path="/">
<ThemeProvider theme={theme}>
<ComponentWithHistory />
</ThemeProvider>
</Route>
</BrowserRouter>
);
}
export default App;
If there are better ways, I would love to hear; but this is working very well for me.

The answer Andrew mentioned works, but there's better ways to do the same thing.
Method 1
Instead of wrapping your component with 'withRouter' and getting the history via props, you can simply use the useHistory hook to do the same.
That would be something like this:
import { useHistory } from "react-router-dom";
const MyComponent = () => {
const history = useHistory();
useEffect(() => {
return () => {
if(history.action === "POP") {
//DO SOMETHING
}
}
});
}
Method 2
Simply use the component provided by react-router.
Use it something like this:
import { Prompt } from "react-router-dom";
const MyComponent = () => {
return (
<>
<div className="root">
//YOUR PAGE CONTENT
</div>
<Prompt message="You have unsaved changes. Do you still want to leave?"/>
</>
);
}
If you want to run some specific code:
<Prompt
message={(location, action) => {
if (action === 'POP') {
//RUN YOUR CODE HERE
console.log("Backing up...")
}
return location.pathname.startsWith("/app")
? true
: `Are you sure you want to go to ${location.pathname}?`
}}
/>
Refer to the docs for more info

Related

Data-fetching with React Server Components: is this a correct implementation?

I'm using Next.JS in an application where I have a Navbar component that is needed on all pages of the application, and the Navbar renders data (specifically product-categories) that must be fetched from my database.
I want this component to be server-side rendered, but since Next.JS only supports page-level SSR and not component-level SSR, it seems that I must use getServerSideProps on all pages that I want to display this Navbar and write the same API request in each one, resulting in a ton of repeated logic and API calls across several pages.
While researching how to solve this, I came across React Server Components and wonder if that would be a valid solution for this scenario, but since I'm new to the concept, I'm not sure if I understand it correctly, hence why I'm writing this question.
I'm thinking of doing the following and want to get some feedback as to whether I am on the right track.
My Navbar component would be something like as follows:
const Navbar = () => {
const [data, setData] = useState();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const data = await fetch("/api/someEndpoint");
setData(data);
};
return (
<div>
{data.someProperty}
{data.someOtherProperty}
</div>
);
};
export default Navbar;
Then I can create a global Layout component so that the Navbar is included in all pages, which is where React.Suspense would be used (if I understand it correctly):
const Layout = ({ children }) => {
return (
<>
<React.Suspense fallback={<FallbackComponent />}>
<Navbar />
<React.Suspense />
{children}
</>
);
};
export default Layout;
Then in _app.tsx, I would include my Layout component so that the Navbar is present everywhere:
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
So here is my question: is this a correct implementation and valid use-case of React.Suspense? Is component-level data fetching one of the proposed benefits of React Server Components?
Now with the Next.JS 13 version, you can create an app directory that uses react server components by default.
But you should keep in mind that most of the React hooks like useState and useEffect won't work on server components, also previous Next.js APIs such as getServerSideProps, getStaticProps, and getInitialProps are not supported in the new app directory.
So instead, you can use the new fetch() API that was built on top of the native fetch() Web API. Check the docs for more details.
Instead of fetching via an API, you could instead use SSR in _app.tsx that would pass the data as props to the MyApp hook. Then the data could be passed down further into the Layout component, which would again pass it down even further to the Navbar component. So it would look like something along the lines of this:
// _app.tsx
function MyApp({ /* Props */, data }) {
return (
<Layout data={data}>
<Component {...pageProps} />
</Layout>
);
}
export function getServerSideProps(context) {
// Get data
return {
props: {
data: data
},
}
}
// Layout.tsx
const Layout = ({ children, data }) => {
return (
<>
<Navbar data={data} />
{children}
</>
);
};
export default Layout;
// Navbar.tsx
const Navbar = ({ data }) => {
return (
<div>
{data.someProperty}
{data.someOtherProperty}
</div>
);
};
export default Navbar;
EDIT: SSR in _app.tsx doesn't work, but I found an answer here: https://stackoverflow.com/a/72325973/12190941
EDIT 2:
You can use getInitalProps in _app.js if you want your entire site to be server side rendered. Do keep in mind that this will increase initial load times. I found this example in the Next.js docs here
// _app.js
// ...
MyApp.getInitialProps = async (appContext) => {
// calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext);
return { ...appProps }

React Navigation 5, block back navigation after login

I am using React Navigation 5 in a project, and I'm having trouble trying to block a user from navigating back after a certain point.
The app uses a nested navigation structure similar to this:
ROOT (STACK)
|-- LoginScreens (STACK - options={{ gestureEnabled: false }} )
| |-- Login (SCREEN) -> when successful navigate to "Home"
| +-- Register (SCREEN) -> after registration, navigate to "Login"
|
+-- Home (TABS - options={{ gestureEnabled: false }} )
|-- BlahBlah (SCREEN)
|-- MyProfile (SCREEN)
+-- Dashboard (TABS)
|-- AllTasks (SCREEN)
+-- SomethingElse (SCREEN)
After a successful user login, the user is sent to the Home screen and should not be able to navigate back to the LoginScreens screen.
I have tried to use the componentDidMount lifecycle method on Home, as well as the useFocusEffect hook, with the following:
Placing a callback to React Native's BackHandler, returning true from the handler works (true means back action has been handled, no further back handlers will be called), but it will also block any back navigation within the screens in Home (e.g. I cannot navigate back from Dashboard to MyProfile).
Using navigation.reset({ index: 1, routes: [{ name: "Home" }] }). Without index: 1 the navigation just goes back to ROOT's initialRoute (in this case, LoginScreens). With index: 1, a Maximum update depth exceeded error is thrown.
Instead navigating directly to Home, I have tried using a navigation.reset() (note: no params, clears the entire navigation history), and after that navigate to the Home screen. This doesn't achieve the desired effect since the current route (ROOT's initialRoute, in this case: LoginScreens) is still pushed on the navigation history before navigating to Home.
Combining navigation and reset calls in different ways, I have only managed to get JS angry and throw errors and exceptions at me.
Aaaaand... I have ran out of ideas. Does anyone have any suggestions ?
It seems that React Navigation's docs tried to cover this use case with this guide:
https://reactnavigation.org/docs/en/auth-flow.html
The example there is very tricky, already introduces state management libraries, reducers, React hooks, and whatever else that doesn't really help. However, the summary of that guide is: Conditionally render routes.
Unlinke React Navigation 4 and previous versions, in React Navigation 5 you can conditionally render routes. In doing so you effectively rule out any possibilities of navigation to an inexistent route. Below, there is a very short example of how you can do it with a simple state variable. Keep in mind however that this example only takes into account a navigator with one route rendered at a time. If you have more routes that are rendered other than the ones in this example, you may need to adjust the RootStack.Navigator's props (initialRouteName for example), or explicitly navigate to a specific route.
import React from "react";
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import LoginNav from "./screens/LoginNav";
import HomeScreens from "./screens/HomeScreens";
const RootStack = createStackNavigator();
export default class MyApp extends React.Component {
constructor(props){
super(props);
this.state = { isLoggedIn: false };
}
setIsLoggedIn = (isLoggedIn)=>{ this.setState({ isLoggedIn }); }
render = () => {
// Using an arrow function to allow to pass setIsLoggedIn to LoginNav
// Pass setIsLoggedIn from the props of LoginNav to the screens it contains
// then from the screens call this function with a true/false param
const LoginScreens = (props)=> <LoginNav {...props} setIsLoggedIn={this.setIsLoggedIn} />
return <NavigationContainer style={{ flex: 1 }}>
<RootStack.Navigator>
{(this.state.isLoggedIn === false)
// If not logged in, the user will be shown this route
? <RootStack.Screen name="LoginScreens" component={LoginScreens} />
// When logged in, the user will be shown this route
: <RootStack.Screen name="Home" component={HomeScreens} />
}
</RootStack.Navigator>
</NavigationContainer>;
}
}
In this example, call (this.) props.setIsLoggedIn(true) to render the Home route, or call with a false param to return to the LoginScreens route.
Hopefully this example is easier to understand than the one in the docs.
Well, I have to admit, its was not easy to find the new reset method's syntax for v5, man... ReactNavigation docs really need an in site search functionality.
Anyway, reset method can be used, and worked perfectly for me.
It looks something like:
import { CommonActions } from '#react-navigation/native';
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [
{
name: 'Home',
params: { user: 'jane' },
},
],
})
);
I made a helper function which I am using in multiple places in my app, that looks like:
import { CommonActions } from '#react-navigation/native';
export const resetStackAndNavigate = (navigation, path) => {
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: path }] }));
};
I've done it this way for react-navigation v5:
I've created a CustomDrawerContent-Component to be able to handle each press on an item:
(Note: Ignore header and footer property, it's only an adjustment for my drawer.)
...
import {
DrawerContentScrollView,
DrawerItem,
} from '#react-navigation/drawer';
...
function CustomDrawerContent(props) {
const {
state: {routes, index},
descriptors,
navigation,
header,
footer,
} = props;
return (
<>
{header}
<DrawerContentScrollView {...props}>
{routes.map((route, i) => {
const focused = i === index;
const {title, drawerLabel, drawerIcon} = descriptors[
route.key
].options;
return (
<DrawerItem
key={route.key}
label={
drawerLabel !== undefined
? drawerLabel
: title !== undefined
? title
: route.name
}
icon={drawerIcon}
focused={focused}
onPress={() => {
navigation.dispatch(
CommonActions.reset({index: i, routes: [{name: route.name}]}),
// NOTICE: Removes the routes.<name>.state of the Stack to discard
// navigation-Position if coming back to it via Drawer-Menu.
// If this Stack-State in seeded later on, you can adjust it here needed
);
}}
/>
);
})}
</DrawerContentScrollView>
{footer}
</>
);
}
function MainDrawer(props) {
const {
screen,
screen: {initialRoute},
navigatorProps,
header,
footer,
hideDrawerItems,
} = props;
return (
<Navigator
initialRouteName={initialRoute}
{...navigatorProps}
drawerContent={(drawerProps) => (
<CustomDrawerContent {...drawerProps} header={header} footer={footer} />
)}>
{createMenuEntries(screen, hideDrawerItems)} // that's only an custom implementation of mine to create <Screen>-Entries. Feel free to replace it with your own
</Navigator>
);
}
export default MainDrawer;
The Magic at least is here:
{routes.map((route, i) => {
...
onPress => navigation.dispatch => CommonActions.reset({index: ⇒⇒ i ⇐⇐
While we map over each route, we use the current index and the route-name (of the drawer-item itself) to reset it's route-state, if we tapped it.
This works perfectly fine for my purposes, because even if you are in News ⇒ News Detail"and open the Drawer and click again on News, you are piped to the first Screen of your News-Stack.
I like the accepted solution, but the other way to do this is to use React Context.
const AuthContext = React.createContext();
const setIsLoggedIn = (isLoggedIn) => {
this.setState({ isLoggedIn });
}
and then wrap your entire navigator:
<AuthContext.Provider value={setIsLoggedIn}>
<RootStackNavigator>
// your screens, etc.
</RootStackNavigator>
</AuthContext.Provider>
then in your screen, you can use:
const { setIsLoggedIn } = React.useContext(AuthContext);
and call it when you want.
See this guide: https://reactnavigation.org/docs/auth-flow/
Initially I had posted this solution:
https://stackoverflow.com/a/60307042/12186963
However, eventually, I ended up not using it due to some serious jank issues I had with conditional rendering:
When aNavigator / Screen mounts, a lot of stuff happens, multiple screens might get instantiated (especially if you're using tabbed navigators without lazy mount), nested <Navigator />s might mount, react-navigation has to re-evaluate it's state, and much, much more.
The app does not have a choice but hold on until the entire route tree mounts before rendering it, which can cause blank flashes between mounts. On lower-end devices, the blank screen can persist for longer times than a user would tolerate.
The better alternative solution that I have found involves the imperative call to NavigationContainer.resetRoot method. By attaching a ref to the NavigationContainer, calling resetRoot will always act on the root navigation state.
resetRoot also allows to specify a new navigation state, which can be useful to change the currently active route.
The implementation is as follows:
libs/root-navigation.js
import React from "react";
// This is the ref to attach to the NavigationContainer instance
export const ref = React.createRef();
/**
* Resets the root navigation state, and changes the active route to the one specified
* #param {string} name The name of the route to navigate to after the reset
* #param {object|undefined} params Additional navigation params to pass to the route
*/
export function navigate(name, params) {
try {
ref.current.resetRoot({ index: 0, routes: [{ name, params }] });
} catch (e) {
console.error("Failed to reset the root navigation state. Make sure you have correctly attached the ref to the <NavigationContainer /> component.\nOriginal error:", e);
}
}
App.js(or wherever you render your<NavigationContainer /> component:
import { NavigationContainer } from "#react-navigation/native";
import * as RootNavigation from "./libs/root-navigation";
import { createStackNavigator } from "#react-navigation/stack";
import LoginScreen from "./screens/Login";
import RegisterScreen from "./screens/Register";
import DashboardScreen from "./screens/Dashboard";
import AccountScreen from "./screens/Account";
const RootStack = createStackNavigator();
const AuthenticationStack = createStackNavigator();
const HomeStack = createStackNavigator();
function AuthenticationScreens() {
return <AuthenticationStack.Navigator initialRouteName="Login">
<AuthenticationStack.Screen name="Login" component={LoginScreen} />
<AuthenticationStack.Screen name="Register" component={RegisterScreen} />
</AuthenticationStack.Navigator>;
}
function HomeScreens() {
return <HomeStack.Navigator initialRouteName="Dashboard">
<HomeStack.Screen name="Dashboard" component={DashboardScreen} />
<HomeStack.Screen name="Account" component={AccountScreen} />
</HomeStack.Navigator>;
}
export default function MyApp() {
// ... your awesome code :)
return <NavigationContainer ref={RootNavigation.ref}>
<RootStack.Navigator initialRouteName="Authentication">
<RootStack.Screen name="Authentication" component={AuthenticationScreens} />
<RootStack.Screen name="Home" component={HomeScreens} />
</RootStack.Navigator>
</NavigationContainer>;
}
Then, in some other place in your app, you can always import the navigate() function from the root-navigation.js file, and use that to reset the root stack:
import { Pressable, Text, View } from "react-native";
import * as RootNavigation from "./libs/root-navigation";
import * as ServerAPI from "./server-api";
function LoginScreen() {
const email = "hello#world.com";
const password = "P#$sw0rD!";
const onLoginPress = () => {
ServerAPI.login(username, password).then(({ success, user })=>{
if (success === true) {
// Here we reset the root navigation state, and navigate to the "Home" screen
RootNavigation.navigate("Home", { user });
} else {
alert("Wrong email or password...");
}
});
}
return <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Pressable onPress={onLoginPress}>
<Text>Login now!</Text>
</Pressable>
</View>;
}
I much more prefer this solution rather than my initial one. It also works with react-navigation#6.x.

How to async/await on firebase.auth().currentUser?

I am using ReactJs, and defined a Route which will load <Loans />component if the path is mywebsite.com/loans. Below is the code snippet for the <Loans />component. In the componentDidMount, I have async/await to get the currentUser from firebase. If user is null, page will be redirected to /signin page.
class Loans extends Component {
componentDidMount = async () => {
const user = await firebase.auth().currentUser;
if (!user) {
this.props.history.push("/signin");
}
};
render () {
...}
}
Here is the code snippet for <SignIn />component. In SignIn component, there is a listener to listen any auth state change, if user is logged in, page will be redirected to /loanspage.
class SignIn extends React.Component {
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.props.history.push("/loans");
}
});
}
render () {
...
}
}
I actually already logged in. But I observed a weird behavior that whenever I refreshed the page /loans, page will be redirected to /signin page for less than a second and then quickly redirected back to /loans page.
My question is if I already have firebase.auth().currentUser to be async/await, how could I still get null for the user in <Loans /> component, and I only see <Loans /> component when the page is redirected from <SignIn /> page? How can I aviod to see the SignIn page if I already have user logged in in my case. Thanks!
firebase.auth().currentUser isn't a promise, it's a User object, so using await on it doesn't make much sense. According to the API documentation, it's only going to be a User object, or null. It will be null when there is no user signed in, or the User object just isn't available yet.
What you should be doing instead is using the same style of listener in SignIn to determine when a User object is ready, and render any content only after that listener indicates a User is present.
I recently built a HOC to handle this. Quick example below
import React from "react"
import { useAuthState } from "react-firebase-hooks/auth"
import { auth } from "./firebase" // Where to store firebase logic
import { Navigate } from "react-router-dom"
export const RequireAuth = ({ children }: { children: JSX.Element }) => {
const [user, loading] = useAuthState(auth)
if (loading) {
return <></>
} else if (user?.uid && !loading) {
return children
} else {
return <Navigate to="/login" />
}
}
Then in the router (I'm using RR v6) you just wrap the page component in the hoc.
<Route
path="/"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
You could also extract this out to a hook and call it in every page instead of at the router level but I feel like this is a bit more readable as far as seeing which routes are protected. This also follows the example of protected routes in the RR docs.

Prevent routing in React when user manually changes url in browser tab

I am stuck in a issue that happens when user manually changes the route in browser tab and presses enter. This forces my react router to navigate to the state entered by user. I want to prevent this and allow routing only through the flow I have implemented by button clicks in my website.
Some of my screens need data that will be available only if the user navigates the site using the flow expected. If user directly tries to navigate to a particular route by manually changing the route in url then he may skip the desired flow and hence the app will break.
Other scenario, in case I want to restrict some users from accessing some routes but the user knows the path and manually enters that in browser url then he will be presented with that screen but should not be.
What I do is use a prop from previous page, if that prop is undefined(meaning user did not follow due process :) hehe ) I simply send the user back to the landing page or wherever.
You can create a route guard using HOC. For example, you don't want unauthorized user to pass route /profile, then you can do the following:
// requireAuthorized.js (HOC)
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {Redirect} from 'react-router-dom'
const connector = connect(
state => ({
isAuthorized: state.profile !== null // say, you keep user profile in redux
})
)
export default (WrappedComponent) => {
return (
connector(
class extends Component {
static propTypes = {
isAuthorized: PropTypes.bool.isRequired
}
render () {
const {isAuthorized, ...clearedProps} = this.props
if (isAuthorized) {
return <WrappedComponent {...clearedProps} />
} else {
return <Redirect to={{pathname: '/login'}} />
}
}
}
)
)
}
// ProfilePage.jsx
import React from 'react'
...
import requireAdmin from '../hocs/requireAdmin' // adjust path
class ProfilePage extends React.Component {
...
render () {
return (
<div>
...
</div>
)
}
}
export default requireAdmin(ProfilePage)
Pay attention to the export statement in my ProfilePage.js
I'd suggest using this library for cleanest solution (or at least make personal similar implementation of it).
Then you'd create authentication check HOC:
export const withAuth = connectedReduxRedirect({
redirectPath: '/login',
authenticatedSelector: state => state.user.isAuthenticated, // or whatever you use
authenticatingSelector: state => state.user.loading,
wrapperDisplayName: 'UserIsAuthenticated'
});
And you could easily create flow HOC:
export const withFlow = (step) = connectedReduxRedirect({
redirectPath: '/initial-flow-step',
authenticatedSelector: state => state.flow[step] === true,
wrapperDisplayName: 'FlowComponent'
});
Then initialize your component
const AuthenticatedComponent = withAuth(Dashboard)
const SecondStepComponent = withFlow("first-step-finished")(SecondStep)
const ThirdStepComponent = withFlow("second-step-finished")(ThirdStep)
You can easily create authenticated flow step by composing HOC:
const AuthSecondStepComponent = withAuth(withFlow("first-step-finished")(SecondStep))
Only thing that is important is that you update your redux state correctly as going through your step flow. When user finishes first step you'd set
state.flow["first-step-finished"] = true // or however you manage your state
so that when user navigates manually to specific page, he wouldn't have that redux state because its an in-memory state and would be redirected to redirectPath route.
Something like this is suitable. You make HOC Route with a wrap to function that deals with authentication/context props.
Note: this deals with direct access to the route, not to the menu items and such. That must be treated in a simmilar way on the menu / menuItem components.
import requireAuth from "../components/login/requireAuth";
class Routes extends React.Component<RoutesProps, {}> {
render() {
return (
<div>
<Switch>
<Route exact={true} path="/" component={requireAuth(Persons, ["UC52_003"])} />
<Route path="/jobs" component={requireAuth(Jobs, ["UC52_006"])} />
</Switch>
</div>
)
}
}
export default function (ComposedComponent, privileges) {
interface AuthenticateProps {
isAuthenticated: boolean
userPrivileges: string[]
}
class Authenticate extends React.Component<AuthenticateProps, {}> {
constructor(props: AuthenticateProps) {
super(props)
}
render() {
return (
isAuthorized(this.props.isAuthenticated, privileges, this.props.userPrivileges) &&
<ComposedComponent {...this.props} /> || <div>User is not authorised to access this page.</div>
);
}
}
function mapStateToProps(state) {
return {
isAuthenticated: state.userContext ? state.userContext.isAuthenticated : false,
userPrivileges: state.userContext ? state.userContext.user ? state.userContext.user.rights : [] : []
};
}
return connect(mapStateToProps, null)(Authenticate);
}
you can put the condition in useEffect of the given page/screen and push it back if it doesnt have the required values.. example below

How to link 'from outside' into Navigator (for example from a global footer)

With React Navigation, is there a way to link from outside to a specific path/screen inside a Navigator?
For example to implement a global footer, like this:
<Provider store={store}>
<View>
<AppNavigator />
<MyFooter /> // Link from here to a path/screen inside AppNavigator
</View>
</Provider>
I think refs might work here. If you want to use Navigator from the same level you declare it you can use react's refs and pass props to MyFooter. Look at example in official documentation.
const AppNavigator = StackNavigator(SomeAppRouteConfigs);
class App extends React.Component {
someFunction = () => {
// call navigate for AppNavigator here:
this.navigator && this.navigator.dispatch({ type: 'Navigate', routeName, params });
}
render() {
return (
<View>
<AppNavigator ref={nav => { this.navigator = nav; }} />
<MyFooter someFunction={this.someFunction} />
</View>
);
}
}
Go to this link:
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
React Navigation Version: 5.x
Sometimes you need to trigger a navigation action from places where you do not have access to the navigation prop, such as a Redux middleware. For such cases, you can dispatch navigation actions from the navigation container.
If you're looking for a way to navigate from inside a component without needing to pass the navigation prop down, see useNavigation.
You can get access to the root navigation object through a ref and pass it to the RootNavigation which we will later use to navigate.
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from './RootNavigation';
export default function App() {
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
In the next step, we define RootNavigation, which is a simple module with functions that dispatch user-defined navigation actions.
// RootNavigation.js
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
// add other navigation functions that you need and export them
Then, in any of your javascript modules, just import the RootNavigation and call functions which you exported from it. You may use this approach outside of your React components and, in fact, it works just as well when used from within them.
// any js module
import * as RootNavigation from './path/to/RootNavigation.js';
// ...
RootNavigation.navigate('ChatScreen', { userName: 'Lucy' });
Apart from navigate, you can add other navigation actions:
import { StackActions } from '#react-navigation/native';
export function push(...args) {
navigationRef.current?.dispatch(StackActions.push(...args));
}
Note that a stack navigators needs to be rendered to handle this action. You may want to check the docs for nesting for more details.
When writing tests, you may mock the navigation functions, and make assertions on whether the correct functions are called with the correct parameters.
Handling initialization
When using this pattern, you need to keep few things in mind to avoid crashes in your app.
The ref is set only after the navigation container renders
A navigator needs to be rendered to be able to handle actions
If you try to navigate without rendering a navigator or before the navigator finishes mounting, it will throw and crash your app if not handled. So you'll need to add an additional check to decide what to do until your app mounts.
For an example, consider the following scenario, you have a screen somewhere in the app, and that screen dispatches a redux action on useEffect/componentDidMount. You are listening for this action in your middleware and try to perform navigation when you get it. This will throw an error, because by this time, the parent navigator hasn't finished mounting. Parent's useEffect/componentDidMount is always called after child's useEffect/componentDidMount.
To avoid this, you can set a ref to tell you that your app has finished mounting, and check that ref before performing any navigation. To do this, we can use useEffect in our root component:
// App.js
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef, isMountedRef } from './RootNavigation';
export default function App() {
React.useEffect(() => {
isMountedRef.current = true;
return () => (isMountedRef.current = false);
}, []);
return (
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
);
}
Also export this ref from our RootNavigation:
// RootNavigation.js
import * as React from 'react';
export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isMountedRef.current && navigationRef.current) {
// Perform navigation if the app has mounted
navigationRef.current.navigate(name, params);
} else {
// You can decide what to do if the app hasn't mounted
// You can ignore this, or add these actions to a queue you can call later
}
}

Resources