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.
Related
I've written a custom ProtectedRoute component for my React app that redirects a user to a /api/login route on my Express API if that user is not authenticated. The /api/login route returns a 301 Redirect to the Auth0 Universal Login UI.
I know the /api/login route on the Express API works because I can hit it directly I get redirected to the Auth0 Universal Login (see the last code snippet).
I also know the ProtectedRoute is redirecting correctly because it redirects to localhost:3000/api/login, which is the correct route on the Express API to trigger the Auth0 Universal Login redirect.
What actually happens though is that localhost:3000/api/login shows up in the address bar but the redirect to the Auth0 Universal Login doesn't happen. That being said if I refresh the page then the redirect to the Universal Login UI works.
I'm not exactly sure why the Redirect returned from /api/login isn't followed in the Browser. I think it has something to do with how React is navigating to the route.
Here's the relevant code snippets. If more are needed let me know.
Protected Route Component
import { Navigate, useLocation } from 'react-router-dom';
interface ISession {
userId: string;
role: string;
details: any;
}
type RouteProps = {
children?: JSX.Element;
session: ISession;
loading: boolean;
};
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading) return null;
else if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
return <Navigate replace to="/api/login" state={{ redirectTo: location.pathname }} />;
}
};
export default ProtectedRoute;
How the ProtectedRoute Component is used with React Router
import { useContext } from 'react';
import { Route, Routes } from 'react-router-dom';
import Home from './pages/home';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import { SessionContext } from './context/SessionContext';
import ProtectedRoute from './middleware/protectedRoute';
const App = () => {
const { session, loading } = useContext(SessionContext);
console.log('Session', session);
return (
<Routes>
<Route element={<ProtectedRoute loading={loading} session={session} />}>
<Route path="/" element={<Home />} />
<Route path="/dne" element={ <p>Stuff</p> } />
</Route>
</Routes>
);
};
export default App;
NOTE: I'm excluding the code from the SessionContext component for brevity. Since the user isn't able to login because the redirect to the Auth0 Universal Login UI doesn't work no session is ever created.
The "/api/login" route handler on the Express API
const login = (req: Request, res: Response) => {
const { redirectTo } = req.query;
const domain = config.get('auth0.domain');
const clientId = config.get('auth0.clientId');
const host = config.get('host');
// These 301 Responses are the Redirect to Auth0's Universal Login UI
if (redirectTo) {
const encodedRedirect = base64.urlEncode(redirectTo as string); // A Custom Base64 encoder that is URL Safe
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback&state=${encodedRedirect}`)
} else {
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback`);
}
};
It's not really an answer for what is going on but it is a solution.
The problem was <Navigate to="/api/login" /> would cause React to Rerender the page and would change the URL in the address bar, but it would not cause the browser to make a GET request to the new address.
To solve this I just overwrote the window.location.href with /api/login in the ProtectedRoute component.
Here's the new version of the ProtectedRoute component.
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading)
return null;
if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
window.location.href = '/api/login';
return <Navigate replace={true} to='/api/login' />
}
};
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
I have an initial redux state like this:
{
loggedInUserId: null,
comments: []
}
Here's how my React App looks like:
class App extends Component {
componentWillMount() {
this.props.getLoggedInUserId();
}
render() {
return (
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/comments" component={Comments} />
</Switch>
);
}
}
In my App, I dispatch an action getLoggedInUserId() which asynchronously fills the loggedInUserId in the state.
The HomePage is a dumb component showing some text. I start the app (route is now '/'), see the HomePage component, then I navigate to the Comments page, which has:
componentWillMount() {
this.props.fetchComments(this.props.loggedInUserId); // Dispatch action to do API call to fetch user's comments
}
render() {
// Show this.props.comments nicely formatted
}
Everything works, I see the list of comments in the Comments component.
But if I refresh the page on the route /comments, then by the time the Comments runs componentWillMount, the loggedInUserId has not been loaded yet, so it will call fetchComments(null).
Right now, to fix this, I'm doing in my Comments component:
componentWillMount() {
if (!this.props.loggedInUserId) return;
this.props.fetchComments(this.props.loggedInUserId);
}
componentWillReceiveProps(nextProps) {
if (!this.props.loggedInUserId && nextProps.loggedInUserId) {
nextProps.fetchComments(nextProps.loggedInUserId);
}
}
which works well. But I'm doing this in 10+ components, and it seems like a lot of work which can be factorized, but I didn't find an elegant way to do it.
So I'm asking you how do you generally deal with this kind of situation? Any idea is welcome:
HOC
side-effects
other libraries
I'm using wrapper around Route, which checks if users are logged in and if not, redirect them to login page. Wrapped routes are rendered only after userId of authenticated user is fetched.
import * as React from 'react'
import { Route, Redirect } from 'react-router-dom'
import URLSearchParams from 'url-search-params'
class AuthRoute extends React.Component {
componentDidMount() {
if (!this.props.isLoading) {
this.props.getLoggedInUserId()
}
}
render() {
if (this.props.isLoading) {
// first request is fired to fetch authenticated user
return null // or spinner
} else if (this.props.isAuthenticated) {
// user is authenticated
return <Route {...this.props} />
} else {
// invalid user or authentication expired
// redirect to login page and remember original location
const search = new URLSearchParams({
next: this.props.location.pathname,
})
const next =
this.props.location.pathname !== '/' ? `?${search.toString()}` : ''
return <Redirect to={`/login${next}`} />
}
}
}
You need to update your reducer which handle getLoggedInUserId action to store also isLoading state.
You probably want the initial state to be rendered by the server into 'index.html' (or what have you) and hydrated on the client.
This initial state would include loggedInUserId and data for the /comments page.
Check out https://redux.js.org/docs/recipes/ServerRendering.html
I think using HOC will be clean here. As all the common logic will be at the same place. Use composition here
Let say you have components A, B, C, D
Now you want to write some common function on the componentWillReceiveProps lifecycle of all the components.
Write a HOC like:
class HOC extends React.Component {
componentWillReceiveProps(nextProps) {
//Your commomn logic
}
render() {
const childrenWithProps = React.Children.map(this.props.children,
child => React.cloneElement(child, {
...this.props,
})
return (
<div>
{childrenWithProps}
</div>
)
}
}
Write your components like this:
class A extends React.Component {
componentWillReceiveProps(nextProps) {
//your uncommone logic
}
render(){
return (
<HOC {...this.props}>
<div>
//Your page jsx
</div>
</HOC>
)
}
}
same way write for component B, C, and D. This pattern is useful when there is lot common among components. So better have a look at your usecase
OP writing. After reading nice ideas here, I decided to go with a custom HOC:
import React, { Component } from 'react';
const requireProp = (As, propsSelector, propsToDispatch) =>
class Wrapper extends Component {
componentWillMount() {
if (!propsSelector(this.props) && typeof propsToDispatch === 'function') {
propsToDispatch(this.props);
}
}
render() {
const { ...props } = this.props;
return !!propsSelector(this.props) && <As {...props} />;
}
};
export default requireProp;
To see how I use it, see this gist.
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
I have a few routes setup like so:
import createHistory from 'history/createMemoryHistory';
import { NativeRouter, Route } from 'react-router-native';
<NativeRouter history={createHistory()}>
<View style={styles.container}>
<Route exact path='/' component={Home}/>
<Route path='/page1' component={Page1}/>
</View>
</NativeRouter>
Now it's simple to change routes programmatically in my Home class for example:
this.props.history.push('/page1');
But in the case of an async action that I'm dispatching with Redux:
export function login(email, password) {
return function(dispatch) {
dispatch(setAuthBusy(true));
return auth.signIn(email, password)
.then(function (user) {
dispatch(setAuthBusy(false));
dispatch(setUser(user));
// !CHANGE ROUTE HERE!
})
.catch(function (err) {
dispatch(setAuthBusy(false));
dispatch(setAuthError(err.message));
});
};
}
In this case you can see that it's a login action that authenticates a user and only if the user has been successfully authenticated does the route change to /page1 for instance.
I have a feeling that changing routes in your async actions isn't the correct way to go about it so I'd appreciate some advice in terms of the general architecture and flow of the app. Thanks!
If you start to deal with async actions, you should give a look at redux-thunk middleware.
Using redux-thunk you could do the following :
class Home extends React.Component {
this.onLogin = () => {
dispatch(login('toto', 'toto')).then(() => {
this.props.history.push('/page1');
})
}
}
This mean that the route change in your case could be managed by the component itself.
Hope it help.