React Navigation 5, block back navigation after login - reactjs

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.

Related

React Native: Rendered fewer hooks than expected

I'm currently trying to develop an app with multiple screens. Specifically, I'm working on the navigator component that directs the user to the login screen or the home screen based on whether they are logged in or not.
To do this, I'm making use of hooks, React Navigation and Firebase. I have a state which tracks the user, and this state is updated using onAuthStateChanged() from Firebase, which is inside a useEffect hook.
import { useState, useEffect } from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import {
HomeScreen,
LoginScreen,
TimerScreen
} from '../screens';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';
const MainStack = createNativeStackNavigator();
const AppNavigator = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const subscriber = onAuthStateChanged(auth, authUser => {
if (authUser) {
setUser(authUser);
} else {
setUser(null);
}
});
return subscriber;
});
const MainNavigator = () => (
...
);
return (
<NavigationContainer>
{ user ? MainNavigator() : LoginScreen() }
</NavigationContainer>
);
};
export default AppNavigator;
AppNavigator is then called in my App.js:
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<AppNavigator />
</View>
);
}
However, whenever I run the app, I get
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I've read a few posts with the same error message, and a common recommendation is to avoid having hooks inside conditional statements / loops. I did check that my useState and useEffect were at the top level of my component, so that doesn't seem to be the issue.
Right now I'm thinking that the problem could be arising because I'm navigating between screens, but I'll have to look more into it though.
Does anyone know what might be the issue, or any other possible fixes I could try? Any help would be great. Thanks!
user ? MainNavigator() : LoginScreen()
You are calling components as regular functions instead of creating elements from them. To create an element, use the JSX syntax, i.e.:
user ? <MainNavigator /> : <LoginScreen />
(Which will then be transpiled to React.createElement.)
The error occurs because when calling these components as functions, the code inside becomes a part of the render phase of the AppNavigator component. If, for example, MainNavigator contains hooks, and LoginScreen does not, then toggling between which function is (incorrectly) called also changes the number of hooks rendered, as suggested in the error message.

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

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

TypeError: No "routes" found in navigation state

I am using createMaterialTopTabNavigator from react-navigation in which i have two separate screens UpdatesStack and ShopsStack and i want to navigate to other screen from these screens so i written like <Toptab navigation={this.props.navigation} /> and it showing me following red screen error.
And if i write like <Toptab /> then there is no error but i am not able to navigate.
so how can i solve this problem and able to navigate.
code
class Parenthome extends Component {
render() {
const { navigate } = this.props.navigation;
return (
<View style={styles.container}>
<ToolbarAndroid
style={styles.toolbar}
title="Toolbar"
titleColor="#ff6600"
/>
<Toptab navigation={this.props.navigation} />
</View>
);
}
}
const UpdatesStack = createStackNavigator(
{
Updates: { screen: Home }
},
{
initialRouteName: "Updates"
}
);
const ShopsStack = createStackNavigator(
{
Shops: { screen: Conshop }
},
{
initialRouteName: "Shops"
}
);
const Toptab = createMaterialTopTabNavigator({
Updatestab: { screen: UpdatesStack },
Shopstab: { screen: ShopsStack }
});
export default Parenthome;
I know it's late but just to answer for those who stumble on this from Search Engines:
Why don't you export default TopTab itself. There seems no need to wrap TopTab with ParentTheme component in your use case. You can style the TopTab navigator itself and render it like any other component.
If you must wrap the TopTab you need to have the router from the TopTab accessible, in addition to the navigation prop. This way they both refer to the same router. Simply put, add in ParentTheme:
static router = TopTab.router;
Check out Custom Navigators for more info. https://reactnavigation.org/docs/en/custom-navigators.html
if you are using functional react components with hooks you won't be able to declare a static variable inside your components because they are not JS classes.
Instead declare the router variable as follows:
const reactComponent = (props) => {
/* your component logic and render here */
}
reactComponent.router = TopTab.router; //equivalent to static variable inside a class
export default reactComponent

Get navigation props in an independent component React Native

My app.js file looks like this
export default class App extends React.Component {
render() {
return (
<Root style={{
flex: 1
}}>
<FcmHandler/>
</Root>
)
}
}
The Root component is where the entire app resides along with all the functionality, the FcmHandler is where I handle functionality related to notifications etc. Within the FcmHandler I have a method that gets a callback when a notification is clicked, inside this callback I need to navigate to a specific screen in the app based on the notification click.
The problem is using the current code above the FcmHandler component never even gets initialized.
If I try something like this
export default class App extends React.Component {
render() {
return (
<View style={{
flex: 1
}}>
<Root/>
<FcmHandler/>
</View>
)
}
}
the FcmHandler component gets called but I do not have any access to navigation props which reside inside the <Root/> component.
The <Root/> component consists of the following
const ArticleStack = StackNavigator(
{
...
}
);
const SettingsStack = StackNavigator({
...
});
export const Root = StackNavigator({
Articles: {
screen: ArticleStack
},
Settings: {
screen: SettingsStack
},
}, {
mode: 'modal',
headerMode: 'none'
});
The basic goal I am trying to achieve is, when a notification is click, irrespective of which screen the app is currently on I should be able to navigate to a particular screen. I do not want to write the navigation code in every screen component that I have, that seems redundant.
You can follow this official guide to create your navigation service. Then use the navigation service in FcmHandler instead of navigation prop. This way there is no need to put FcmHandler as a child of the navigator.
If you are using redux or mobx, it's better to move your navigation state to the store for easier access. For redux, there is an official integration guide. For mobx, you can try this.
For react-navigation users, a really cool way is to create your own Navigation Service
You can initialize your Navigation Service module, during the time initializing your navigation store as mentioned in their docs
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
addListener,
})} />
// Just add another line to config the navigator object
NavigationService.configNavigator(dispatch) <== This is the important part
NavigationService.js
import { NavigationActions } from 'react-navigation'
let config = {}
const configNavigator = nav => {
config.navigator = nav
}
const reset = (routeName, params) => {
let action = NavigationActions.reset({
index: 0,
key: null,
actions: [
NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName,
params,
}),
],
})
config.navigator(action)
}
const navigate = (routeName, params) => {
let action = NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName,
params,
})
config.navigator(action)
}
const navigateDeep = actions => {
let action = actions.reduceRight(
(prevAction, action) =>
NavigationActions.navigate({
type: 'Navigation/NAVIGATE',
routeName: action.routeName,
params: action.params,
action: prevAction,
}),
undefined
)
config.navigator(action)
}
const goBack = () => {
if (config.navigator) {
let action = NavigationActions.back({})
config.navigator(action)
}
}
export default {
configNavigator,
navigateDeep,
navigate,
reset,
goBack,
}
Explanation :
The config initializes the navigator's dispatch object whenever your redux-navigation gets initialzed, therefore you can dispatch any navigation action, wrt the method's present in the Service Component.
Use
NavigationServices.navigate('ScreenName')
Update:
React Navigation now provides a HOC wrapper withNavigation, that passes the navigation prop into a wrapped component.
It's useful when you cannot pass the navigation prop into the component directly, or don't want to pass it in case of a deeply nested child.
Usage is well mentioned in their docs.
After a bit of research, the easiest way I found was to follow their official documentation:
I created a RootNavigation.js file in the ./misc folder;
import * as React from 'react';
export const navigationRef = React.createRef();
export function navigate(name, params) {
navigationRef.current?.navigate(name, params);
}
I imported it into App.js and created a reference to it in the return function:
import React from 'react'
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { navigationRef } from './misc/rootNavigation'; <- navigationRef is imported
…
const Stack = createStackNavigator();
function App() {
return (
<Provider store={store}>
<NavigationContainer ref={navigationRef}> <— reference to navigationRef
<Stack.Navigator>
…
<Stack.Screen
name="Screen"
component={Screen}
options={{
title: “Hello”,
headerLeft: () => <ScreenButton/>
}} />
</Stack.Navigator>
</NavigationContainer>
</Provider>
);
}
export default App
I called it inside the ScreenButton component
import React, { Component } from 'react'
…
import * as RootNavigation from '../misc/rootNavigation'; <—- imported
class RoomButton extends Component {
constructor(props) {
super(props)
}
render() {
return (
<TouchableOpacity onPress={
() => {RootNavigation.navigate( 'RoomSelectorScreen' ) <—- called here
…
</TouchableOpacity>
)
}
}

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

Resources