This question already has answers here:
How can I block a React component to be rendered until I fetched all informations?
(6 answers)
PrivateRoute - how to wait a response from async?
(4 answers)
Closed 2 years ago.
Hello im trying to create a private Route
PrivateRoute.js
const PrivateRoute = ({ children, ...rest }) => {
return (
<Route
{...rest}
render={({ location }) =>
Authenticate() ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location },
}}
/>
)
}
/>
);
};
export default PrivateRoute;
When I request a private route, the child component renders and after a while the Autheticate function is completed. In the Authenticate function I make a request that returns the token data and saves it in the sessionStorage
Authenticate.js
const Authenticate = async () => {
let token = localStorage.getItem("token");
if (token) {
token = "Bearer " + token;
let response = await tokenRequest(token);
if (response) {
if (response.status === 200) {
await sessionStorage.setItem(
"userData",
JSON.stringify(response.data.token)
);
return true;
} else {
localStorage.clear();
sessionStorage.clear();
return false;
}
} else {
return false;
}
} else {
localStorage.clear();
return false;
}
};
export default Authenticate;
How could I finish the function's task and then redirect to the child component?
Within your login route, you can use the history api's state that you are setting in your redirect to navigate back once the user logs in. The state gets stored in the location variable in react router dom. https://reacttraining.com/react-router/web/api/location
Other than that, your Authenticate function currently returns a Promise of a boolean. Promises are always truthy, so your Redirect is likely never being run.
For the logic of how to get the PrivateRoute to work with asynchronous checks, I'll refer you to this which is from a similar issue: authorized route vs unauthorized routes reactjs not properly loading
Related
I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.
I am building a Next.js project where I want to implement private route similar to react-private route. In React this can be done by using react-router, but in Next.js this cannot be done. next/auth has the option to create private route I guess, but I am not using next/auth.
I have created a HOC for checking if the user is logged-in or not, but I'm not able to redirect the user to the private he/she wants to go after successfully logging in. How to achieve this functionality in Next.js? Can anybody help me in this?
This is the HOC, I used the code from a blog about private routing.
import React from 'react';
import Router from 'next/router';
const login = '/login'; // Define your login route address.
/**
* Check user authentication and authorization
* It depends on you and your auth service provider.
* #returns {{auth: null}}
*/
const checkUserAuthentication = () => {
const token = typeof window !== "undefined" && localStorage.getItem('test_token');
if(!token) {
return { auth: null };
} else return {auth:true};
// change null to { isAdmin: true } for test it.
};
export default WrappedComponent => {
const hocComponent = ({ ...props }) => <WrappedComponent {...props} />;
hocComponent.getInitialProps = async (context) => {
const userAuth = await checkUserAuthentication();
// Are you an authorized user or not?
if (!userAuth?.auth) {
// Handle server-side and client-side rendering.
if (context.res) {
context.res?.writeHead(302, {
Location: login,
});
context.res?.end();
} else {
Router.replace(login);
}
} else if (WrappedComponent.getInitialProps) {
const wrappedProps = await WrappedComponent.getInitialProps({...context, auth: userAuth});
return { ...wrappedProps, userAuth };
}
return { userAuth };
};
return hocComponent;
};
This is my private route code:
import withPrivateRoute from "../../components/withPrivateRoute";
// import WrappedComponent from "../../components/WrappedComponent";
const profilePage = () => {
return (
<div>
<h1>This is private route</h1>
</div>
)
}
export default withPrivateRoute(profilePage);
To redirect back to the protected route the user was trying to access, you can pass a query parameter with the current path (protected route path) when redirecting to the login page.
// Authentication HOC
const loginPath = `/login?from=${encodeURIComponent(context.asPath)}`;
if (context.res) {
context.res.writeHead(302, {
Location: loginPath
});
context.res.end();
} else {
Router.replace(loginPath);
}
In the login page, once the login is complete, you can access the query parameter from router.query.from and use that to redirect the user back.
// Login page
const login = () => {
// Your login logic
// If login is successful, redirect back to `router.query.from`
// or fallback to `/homepage` if login page was accessed directly
router.push(router.query.from && decodeURIComponent(router.query.from) ?? '/homepage');
}
Note that encodeURIComponent/decodeURIComponent is used because the asPath property can contain query string parameters. These need to be encoded when passed to the login URL, and then decoded back when the URL is used to redirect back.
I'm performing everything which is needed for logout (async http request, localstorage + state update) within an reducer (+ AsyncThunk). The only thing I'm missing is to also perform the required redirect to the login page from within this "centralized context".
Here's the main code involved:
http.service.ts
// ...
const httpClient: AxiosInstance = axios.create({ /* ... */ });
httpClient.interceptors.response.use(undefined, async function (error) {
if (401 === error.response.status) {
// TODO: How to logout incl. http request, localstorage + state update & redirect?
}
return Promise.reject(error);
});
// ...
auth.service.ts
// ...
class AuthService {
// ...
async logout(): Promise<void> {
localStorage.removeItem("User");
await httpClient.post(ApiRoutesService.postLogout());
}
}
// ...
auth.slice.ts
// ...
export const logout = createAsyncThunk("auth/logout", async () => {
await AuthService.logout();
});
// ...
const authSlice = createSlice({
// ...
extraReducers: (builder) => {
builder
// ...
.addCase(logout.fulfilled, (state, _action) => {
state.isLoggedIn = false;
state.id = emptyUser.id;
state.email = emptyUser.email;
state.fullName = emptyUser.fullName;
});
},
});
// ...
app-header.tsx
// ...
export default function AppHeader(): JSX.Element {
// ...
const onLogout = useCallback(() => {
dispatch(logout());
}, [dispatch]);
return (
<nav>
<ul>
{/* ... */}
<li>
<Link to={RouterService.paths.login} onClick={onLogout}>
Logout
</Link>
</li>
</ul>
{/* ... */}
</nav>
);
}
The problem
ATM there are 2 places where I'd like to perform the whole logout incl. http request, localStorage + state update and redirect to login page:
From within component AppHeader
From "401 interceptor" on every http request
Whilst all of required "actions" are already happening inside AppHeader I don't know who to do all this from within http.service.ts.
The questions
How can I dispatch the logout action from within http.service.ts on 401 interception?
In other words: How to dispatch actions from a "raw TS service" (or other "non react component files") when not having access to the useDispatch hook?
I already tried directly using the store's dispatch method using import { store } from '.../store' + store.dispatch(logout); but that gave me rather hard to understand runtime errors.
How should I perform the router redirect from within the 401 interceptor?
This basically comes down to the same questions as above: How to perform the redirect when not having access to the useHistory hook? Is there some way to "directly" access the router history?
In addition to the above: Should I rather perform the router redirect directly from within the logout AsyncThunk instead of doing it from everywhere the reducer is called (ATM AppHeader & the 401 interceptor)?
If it is recommended to perform the redirect from the AsyncThunk: How to use the router history (same question as above...)?
Is all of the above "nonsense"? If so, what are actual best practices here?
FYI
I'm quite new to the whole React ecosystem and am just learning all the concepts involved. The shown code was pretty much copy pasted & adjusted from various online resources and whilst I do understand the basics involved I only have a somewhat shallow understanding of all the details regarding redux toolkit, hooks, etc.
I solved this by combining some advice I found in other SO questions and internet resources (blogs etc.):
Overview
"Inject" the redux store after creation into the http service to allow it to dispatch actions (logout here)
Create an explicit ProtectedRoute component which is used instead of the routers default Route for routes which need the user to be authenticated.
This route "listens" to changes in the store and performs the redirect either if someone tries to access a protected route without ever being authenticated or if an authenticated user gets logged out during runtime (e.g. because of some expired cookie etc.).
Implementation details
http.service.ts
const httpClient: AxiosInstance = axios.create({ /* ... */ });
// Creation of the interceptor is now wrapped inside a function which allows
// injection of the store
export function setupHttpInterceptor(store: AppStore): void {
http.interceptors.response.use(undefined, async function (error) {
if (401 === error.response.status) {
// Store action can now be dispatched from within the interceptor
store.dispatch(logout());
}
return Promise.reject(error);
});
}
store.ts
// ...
import { setupHttpInterceptor } from '../services/http.service';
// ...
export const store = configureStore({ /* ... */ });
setupHttpInterceptor(store); // Inject the store into the http service
// ...
protected-route.tsx
// ...
type ProtectedRouteProps = { path?: string } & RouteProps;
export function ProtectedRoute(routeProps: ProtectedRouteProps): JSX.Element {
const { isLoggedIn } = useAppSelector((state) => state.auth);
const loginCmpState: LoginLocationState = { referrer: routeProps.path };
// This handles the redirect and also reacts to changes to `auth` state "during runtime"
return isLoggedIn ? (
<Route {...routeProps} />
) : (
<Redirect to={{ pathname: RouterService.paths.login, state: loginCmpState }} />
);
}
app-body.tsx (wherever routes are defined)
// ...
export default function AppBody(): JSX.Element {
return (
<Switch>
<Route path='/login'>
<Login />
</Route>
<ProtectedRoute path='/user-home'>
<UserHome />
</ProtectedRoute>
</Switch>
);
}
// ...
Considerations
I find it a little hacky that the store needs to inject itself into the http service as this creates a kind of coupling between those 2 which is IMHO not very clean. The http service won't work correctly if the store somehow "forgets" to do the interception...
Whilst this basically works, I'm still very open for improvement suggestions!
This question already has answers here:
Programmatically navigate using react router V4
(17 answers)
How to get history on react-router v4?
(7 answers)
Programmatically navigating in React-Router v4
(4 answers)
Closed 4 years ago.
I have a singinUser function which is called when user clicks the submit button. I want the user to redirect to another url when fetching the data is completed. In previous versions of react, I used to use browserHistory from 'react-router'. What should I do in react-router 4?
import axios from 'axios';
const ROOT_URL = 'http://localhost:3090';
export const signinUser = ({ email, password }) => {
return (dispatch) => {
axios.post(`${ROOT_URL}/signin`, { email, password })
.then(response =>{
//I want to redirect
})
.catch(err=>{
})
}
}
This is personally how I perform a redirect for protected routes in my application:
const GuestRoute = ({ component: Component, ...rest }) => (
<WebServiceContextConsumer>
{
context =>
<Route
{...rest}
render={props =>
context.auth.getUser()
? (<Redirect to={{pathname: "/", state: { from: props.location }}} />)
: (<Component {...props} />)
}
/>
}
</WebServiceContextConsumer>
);
You need to separate your view from your logic. Perform your whatever call, then uses promises to catch the response and handle it however you want in your View component.
I am using redux-form, react-redux-firebase, along with react-router-redux and react-router.
When logging in a user with a redux-form connected form, in the onSubmit callback function, I am using the react-redux-firebase function from the withFirebase HOC called props.firebase.login(). Inside the return of this promise, I am attempting to use react-router to navigate to a "protected" view of mine.
This all works well and looks like this:
export default withFirebase(
connect(mapStateToProps, mapDispatchToProps)(
reduxForm({
form: SINGLE_PAGE_FORMS.login,
onSubmit: (values, dispatch, props) => {
const { firebase } = props;
const { username: email, password } = values;
firebase.login({
email,
password,
}).then(response => {
if(!!response) {
dispatch(
openNotificationCenterAC({ message: 'Successfully logged in!', messageType: MESSAGE_TYPE_SUCCESS })
);
// The problem here with login is that the Private Route has not yet seen the Uid update and therefore
// won't load a private route. I need to know the UID is in state before navigating to dashboard.
dispatch( push( ROUTE_OBJS.SECURED[ROUTES.DASHBOARD].path ) );
dispatch( reset(SINGLE_PAGE_FORMS.login) );
}
}).catch(error => {
dispatch(
openNotificationCenterAC({ message: error.message, messageType: MESSAGE_TYPE_ERROR })
);
});
},
})(LoginContainerView)
)
);
The trouble (as seen in comments) is that I have switched over (today) to using a really cool new way of making some of my routes Private. I used the examples found in this link here to create this concept.
My Private component looks like this:
export default class PrivateRouteView extends Component {
render() {
const {
key,
auth,
path,
renderRoute,
showErrorMessage,
} = this.props;
const { uid } = auth;
return (
<Route
exact
key={key}
path={path}
render={
(!!uid && isLoaded(auth) && !isEmpty(auth)) ? renderRoute :
props => {
return (
<Redirect
to={{
pathname: '/login',
state: { from: props.location }
}} />
);
}
} />
);
}
}
The auth prop here is from the withFirebase HOC injection. It's attached to this.props.firebase.auth.
The Problem
Whenever I successfully log in, get the user data returned to me in the .then function of the firebase.login() promise... by the time I get to the dispatch of .push navigation, the PrivateRoute has re-rendered without the new auth object and never re-renders.
I can step through and watch it. It goes:
Login success
Enter into .then function
Notify user of login success
Change navigation to dashboard view
PrivateRoute component re-renders and see's no UID or auth
firebase.login promise entirely resolved
Application state update completes and firebase.auth now has Uid and all other profile/auth objects in state
Container to react-router BrowserRouter, Switch and Route's runs normally
View attached to this container re-renders fine, down to the function where I return the PrivateRoute components
PrivateRoute render method is never reached on the second render, the one where the firebase.login user data finally makes it into firebase.auth object state. So now I am authorized, however it never see's the new auth object because of no rendering...