My Application: Laravel(API SERVER) + React(Frontend)
What I want to do is restrict the access to routes of App if the user is not authenticated.
function App() {
const [userInfo, setUserInfo] = setState({isSignedIn: false});
return (
<Routes>
<Route path="/admin/*" element={ userInfo.isSignedIn? <Admin />: <Navigate replace to="/home/sign-in"/> } />
<Route path="/home/*" element={<Home />} />
</Routes>
);
}
To do that, auth API should be requested before App rendered.
My first try was like...
function App() {
const requestUserInfo = async() => {
const result = await axios.get("/auth");
return result.data;
}
const [userInfo, setUserInfo] = setState(requestUserInfo);
return (
<Routes>
<Route path="/admin/*" element={ userInfo.isSignedIn? <Admin />: <Navigate replace to="/home/sign-in"/> } />
<Route path="/home/*" element={<Home />} />
</Routes>
);
}
However I realized initial state cannot be async.
Also I tried axios in useEffect(), but I think it forces too many rerendering.
So Here Is My Question: What is the most common way to get auth info by API?
Related
today I'm having an issue where react loads the route before my API verifies that the user's JWT token is valid. When using EJS I could pass in a middleware to the route and the middleware would not contain the next() parameter. As a result the server wouldn't render the EJS which is exactly what I want to achieve with react. Also is it possible to make useNavigate not reload when navigating the that specific route?
My routes in App.js currently look like this:
<Route element={<ProtectedRoute access={access}></ProtectedRoute>}>
<Route
path="/login"
exact
element={<Login login={login} access={access}></Login>}
></Route>
<Route
path="/signup"
exact
element={<Signup signup={signup} access={access}></Signup>}
></Route>
<Route
path="/forgot-password"
exact
element={<ForgotPassword access={access}></ForgotPassword>}
></Route>
<Route
path="/reset-password"
exact
element={<ResetPassword access={access}></ResetPassword>}
></Route>
</Route>;
The access function looks like this:
const access = async (token) => {
return await axios.post(
"http://localhost:5000/access",
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
};
The protected route component looks like this:
import { useState, useContext } from "react";
import { useLocation, useNavigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(false);
const { auth } = useContext(AuthContext);
const navigate = useNavigate();
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
if (authorized) {
navigate('/');
} else {
return <Outlet></Outlet>;
}
};
export default ProtectedRoute;
When I use this code my login component renders a bit before the code navigates back to the home page, how do I make the login component not render at all and just make it stay on the home page?
Issue
The ProtectedRoute component's initial authorized state masks the confirmed unauthenticated state, and since the component doesn't wait for authentication confirmation it happily and incorrectly redirects to "/".
The ProtectedRoute component incorrectly issues a navigation action as an unintentional side-effect via the navigate function and doesn't return valid JSX in the unauthenticated case. Use the Navigate component instead.
If the user is authorized the ProtectedRoute should render the Outlet for a protected route to be rendered into, and only redirect to login if unauthorized.
Solution
The ProtectedRoute component should use an indeterminant initial authorized state that doesn't match either the authenticated or unauthenticated state, and wait for the auth status to be confirmed before rendering either the Outlet or Navigate components.
Example:
import { useState, useContext } from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const location = useLocation();
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
};
Move the login route outside the ProtectedRoute layout route.
<Routes>
<Route
path="/login"
element={<Login login={login} access={access} />}
/>
<Route
path="/signup"
element={<Signup signup={signup} access={access} />}
/>
<Route
path="/forgot-password"
element={<ForgotPassword access={access} />}
/>
<Route
path="/reset-password"
element={<ResetPassword access={access} />}
/>
... other unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected routes ...
</Route>
</Routes>
To protect the login/signup/forgot/reset/etc routes
Create an AnonymousRoute component that inverts the Outlet and Navigate components on the authentication status. This time authenticated users get redirected off the route.
const AnonymousRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Navigate to="/" replace />
: <Outlet />;
};
...
<Routes>
<Route element={<AnonymousRoute access={access} />}>
<Route path="/login" element={<Login login={login} access={access} />} />
<Route path="/signup" element={<Signup signup={signup} access={access} />} />
<Route path="/forgot-password" element={<ForgotPassword access={access} />} />
<Route path="/reset-password" element={<ResetPassword access={access} />} />
... other protected anonymous routes ...
</Route>
... unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected authenticated routes ...
</Route>
</Routes>
I am new to localStorage and React Router, and my goal is:
Redirect user to the "/dashboard" when he is logged in, and Redirect back to '/home' when he is logged out. Also, of course, not allowing him to go to the 'dashboard' if he is not logged in. For some reason my code in App.js not working:
function App() {
let userLogged;
useEffect(() => {
function checkUserData() {
userLogged = localStorage.getItem("userLogged");
}
window.addEventListener("storage", checkUserData);
return () => {
window.removeEventListener("storage", checkUserData);
};
}, []);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home />} />
{userLogged ? (
<Route path={"/dashboard"} element={<Dashboard />} />
) : (
<Route path={"/home"} element={<Home />} />
)}
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
I set it in the home and dashboard pages by localStorage.setItem('userLogged', false) and localStorage.setItem('userLogged', true)
You can only listen to changes in localStorage from other window/browser contexts, not from within the same browser/window context. Here it's expected the window knows its own state. In this case, you actually need some React state.
Convert the userLogged to a React state variable and use a useEffect hook to initialize and persist the userLogged state to/from localStorage. Instead of conditionally rendering Route components, create a wrapper component to read the userLogged value from localStorage and conditionally render an Outlet for nested/wrapped routes or a Navigate component to redirect to your auth route to login.
Example:
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation(); // current location
const userLogged = JSON.parse(localStorage.getItem("userLogged"));
return userLogged
? <Outlet />
: (
<Navigate
to="/"
replace
state={{ from: location }} // <-- pass location in route state
/>
);
};
...
function App() {
const [userLogged, setUserLogged] = useState(
JSON.parse(localStorage.getItem("userLogged"))
);
useEffect(() => {
localStorage.setItem("userLogged", JSON.stringify(userLogged));
}, [userLogged]);
const logIn = () => setUserLogged(true);
const logOut = () => setUserLogged(false);
return (
<div className="App">
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home logIn={logIn} />} />
<Route path={"/home"} element={<Home logIn={logIn} />} />
<Route element={<AuthWrapper />}>
<Route path={"/dashboard"} element={<Dashboard />} />
</Route>
</Routes>
</Router>
</React.StrictMode>
</div>
);
}
export default App;
Home
import { useLocation, useNavigate } from 'react-router-dom';
const Home = ({ logIn }) => {
const { state } = useLocation();
const navigate = useNavigate();
const loginHandler = () => {
// authentication logic
if (/* success */) {
const { from } = state || {};
// callback to update state
logIn();
// redirect back to protected route being accessed
navigate(from.pathname, { replace: true });
}
};
...
};
You can render both route and use Navigate component to redirect. Like this -
// [...]
<Route path={"/dashboard"} element={<Dashboard />} />
<Route path={"/home"} element={<Home />} />
{
userLogged ?
<Navigate to="/dashboard" /> :
<Navigate to="/home" />
}
// other routes
Whenever you logout, you need to manually redirect to the desired page using useNavigate hook.
I have a React project that has a HeaderComponent that exists for all routes in project like this:
function App() {
return (
<Fragment>
<Router>
<HeaderComponent />
<Routes>
<Route path="/login" element={<Login />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/" element={<LandingPage />}></Route>
</Routes>
<FooterComponent />
</Router>
</Fragment>
);
}
And my problem is that the <HeaderComponent> is rendered when the website first loads but when the user logs in, the <HeaderComponent> is not aware of the changes because the component has already mounted.
So in my <HeaderComponent>, the componentDidMount function looks like this:
componentDidMount() {
AuthService.authorizeUser()
.then((r) => {
this.setState({ loggedIn: true });
})
.catch((error) => {
this.setState({ loggedIn: false });
});
}
This only works if I refresh the page.
Basically, if a user successfully logs in (from the <Login> component), what is the proper way of making my HeaderComponent aware of this?
You can use Context API to make AuthContext to share global state within your app:
// AuthContext.js
export const AuthContext = React.createContext({});
export const AuthProvider = ({
children,
}) => {
// your context logic
return (
<AuthContext.Provider value={yourAuthValue}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => React.useContext(AuthContext);
// Layout.js
import { Outlet } from 'react-router-dom'
// Using `Outlet` to render the view within layout
export const Layout = () => {
return (
<>
<HeaderComponent />
<Outlet />
<FooterComponent />
</>
)
}
// HeaderComponent.js
import { useAuth } from './AuthContext'
export const HeaderComponent = () => {
// get state from auth context
const { isLoggedIn } = useAuth()
return // rest of your code
}
// App.js
function App() {
return (
<Fragment>
<-- Wrap your app with AuthContext let other components within your app can access auth state !-->
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
</Fragment>
);
}
There are a couple of ways to do so.
When you're facing a situation where you need to share the same state between multiple components, lifting the state up should be the first thing to try Check this codesandbox.
And some great blogposts to read, KCD - Prop Drilling, KCD - State Management with React
Such approach may cause "prop drilling" when you need the same state in deeply nested components and that's where the context API comes in handy.
codesandbox
I use 'React Context' to pass state user to the child components.
The problem: Everytime you reload the page, the state user value is null. This cause the page briefly redirect to /login before redirecting to '/dashboard`. This will prevent user from accessing a page manually.
The goal: How to wait for state user before rendering?
App.js
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) setUser(user);
else setUser(null);
});
}, []);
return (
<Router>
<AuthDataContext.Provider value={user}>
<Layout>
<Routes />
</Layout>
</AuthDataContext.Provider>
</Router>
);
}
routes
export const Routes = () => {
const user = useContext(AuthDataContext);
if (user) {
return (
<Switch>
<Route path="/dashboard" component={Dashboard} />
<Route path="/list" component={List} />
<Redirect to="/dashboard" />
</Switch>
);
} else {
return (
<Switch>
<Route path="/login" component={LogIn} />
<Route path="/register" component={Register} />
<Route path="/passwordreset" component={PasswordReset} />
<Redirect to="/login" />
</Switch>
);
}
};
authdata
import React from "react";
export const AuthDataContext = React.createContext(null);
edit: quick fix
App.js
const [user, setUser] = useState("first");
routes
else if (user === "first") return null
You can have a component that initiates API call and renders some kind of loader while user is waiting for a response. Then in case user is authenticated you can render regular application routes, if not – render routes for sign in, sign up and so on.
Im trying to figure out how to structure a Router to use different routes for admin, user and public.
I have seen this post and the answer describing a key cloak - but I haven't been able to make sense of it.
I've seen this code sandbox which looks logical to me, but I'm having trouble incorporating it.
I have a constants file where I and defining routes as:
export const NEWBLOG = '/admin/newblog';
export const VIEWBLOG = '/viewblog';
I'm importing that into my App.js and then wanting to define different consts for Admin, User and Public as follows:
import * as ROUTES from '../../util/constants/Routes';
import NewBlog from '../../components/blog/admin/New';
// admin routes
const Admin = ({ match }) => (
<React.Fragment>
<Route path={`${match.path}/${ROUTES.NEWBLOG}`} component={NewBlog} />
<Route path={`${match.path}/2`} render={() => <h2>test</h2>} />
</React.Fragment>
);
// authenticated user routes
const Other = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/2`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/2`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
// public routes
const Public = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/2`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/2`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
Then inside the router statement I have:
const App = () => (
<Router>
<Navigation />
<Switch>
<Route path="/a" component={Admin} />
<Route path="/u" component={Other} />
<Route path="/p" component={Public} />
<Route
component={({ location }) => {
return (
<div
style={{
padding: "50px",
width: "100%",
textAlign: "center"
}}
>
<ErrorMessage />
</div>
);
}}
/>
</Switch>
</Router>
);
export default App;
This all works until I try to use the routes constants inside the back ticks part of the Admin constant.
I can't seem to use that approach.
Can anyone help with a source of reference materials to find a way through this?
There are few things you need to know
Child Routes will render only when the Parent route path is matched
For the Child Route the path needs to be the path that matched the parent + the child route path
You can write wrappers over route which are responsible for deciding if the user is authenticated or an admin
In all such scenarios you need to store the user authentication state within state, context or redux store.
When you render the Route in Admin like
<Route path={`${match.path}/${ROUTES.NEWBLOG}`} component={NewBlog} />
The path to the component actually becomes /a/admin/newBlog which is actually incorrect
Overall you can change your code to something like this
App.js
const App = () => (
<Router>
<Navigation />
<Switch>
<Route path="/admin" component={Admin} />
<Route path="/user" component={Other} />
<Route path="/public" component={Public} />
</Switch>
</Router>
);
AuthRoute.js
const AuthRoute = (props) => {
const {path, match, component: Component, render, ...rest} = props;
const {user, isLoading} = useContext(AuthContext); // Assuming you use context to store route, you can actually get this values from redux store too.
return (
<Route
{...rest}
path={`${match.path}${path}`}
render={(routerProps) => {
if(isLoading) return <div>Loading...</div>
if(!user) return <div>Not Authenticated</div>
return Component? <Component {...rest} {...routerProps} /> : render(routerProps)
}}
/>
}
An adminRoute needs to both check whether the user is admin as well as check if he is authenticated or not so you component would look like
AdminRoute.js
const AdminRoute = (props) => {
const {path, match, ...rest} = props;
const {user, isLoading} = useContext(AuthContext); // Assuming you use context to store route, you can actually get this values from redux store too.
return (
<Route
{...rest}
path={`${match.path}${path}`}
render={(routerProps) => {
if(isLoading) return <div>Loading...</div>
if(!user) return <div>Not Authenticated</div>
if(user.role !== "admin") return <div>Need to be an admin to access this route</div>
return Component? <Component {...rest} {...routerProps} /> : render(routerProps)
}}
/>
}
Now you can use the above two components to separate out the Admin and Auth Routes
Also keep in mind that AuthRoutes and public routes paths cannot be the same
Route constants
export const NEWBLOG = '/newblog';
export const VIEWBLOG = '/viewblog';
Routes
import * as ROUTES from '../../util/constants/Routes';
import NewBlog from '../../components/blog/admin/New';
// admin routes
const Admin = (props) => (
<React.Fragment>
<AdminRoute {...props} path={ROUTES.NEWBLOG} component={NewBlog} />
<AdminRoute {...props} path='/2' render={() => <h2>test</h2>} />
</React.Fragment>
);
// authenticated user routes
const Other = (props) => (
<React.Fragment>
<Switch>
<AuthRoute {...props} path={'/3'} render={() => <h2>one</h2>} />
<AuthRoute {...props} path={'/4'} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);
// public routes
const Public = ({ match }) => (
<React.Fragment>
<Switch>
<Route path={`${match.path}/5`} render={() => <h2>one</h2>} />
<Route path={`${match.path}/6`} render={() => <h2>two</h2>} />
</Switch>
</React.Fragment>
);