I have some state that gets updated async that I would like to wrap inside a function call (to modularize the code).
Concretely, I have a isLoggedIn state variable that I would like to retrieve with a function call inside a React component. So far I have:
firebase.js
function userLoggedIn() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
firebase.auth().onAuthStateChanged((user) => {
if (user == null) {
console.log("No user logged in.");
} else {
console.log(`User ${user.uid} authenticated!`);
setIsLoggedIn(true);
}
});
return isLoggedIn;
}
export default userLoggedIn;
And I have a separate component that I would like to get this isLoggedIn state from:
AuthenticatedRoute.js
const AuthenticatedRoute = ({ component: Component, ...rest }) => {
const isLoggedin = userLoggedIn();
return (
<Route
{...rest}
render={(props) =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
};
It works fine on page load, but if I log out (and user = null) isLoggedIn doesn't change to False.
This all works fine if I inline the function body inside AuthenticatedRoute: in general, how does one achieve this?
Related
I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.
i seem to be getting the following error when using useEffect hook.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I believe it has something to do with the async function i am calling to set if the user is authenticated or not.
ProtectedRoute.tsx
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
isUserAuthenticated(context.token).then(setIsAuthenticated).catch(setIsAuthenticated);
});
if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
const isUserAuthenticated = async (token: any): Promise<boolean> => {
try {
const response = await VerifyAuthentication(token);
console.log("VerifyAuthentication", response);
if (response.status !== 200) {
return false;
}
return true;
} catch (err) {
return false;
}
};
App.tsx
class App extends Component<Props, State> {
renderRouter = () => {
return (
<Router>
<Switch>
<ProtectedRoute exact path="/" component={Dashboard} />
<Route exact path="/login" component={Login} />
</Switch>
</Router>
);
};
render(): ReactElement {
return (
<div className="App">
<AppProvider>
<Theme>
<Sidebar>{this.renderRouter()}</Sidebar>
</Theme>
</AppProvider>
</div>
);
}
}
Presumably this redirects the user to a route which doesn't have this component:
return <Redirect to={{ pathname: "login" }} />;
Which means the component is unmounted, or generally unloaded from active use/memory. And this always happens, because this condition will never be true:
if (isAuthenticated) {
Because when the component first renders that value is explicitly set to false:
const [isAuthenticated, setIsAuthenticated] = useState(false);
So basically what's happening is:
You fire off an asynchronous operation to check if the user is authenticated.
Before waiting for the response, you decide that the user is not authenticated and redirect them.
The component is unloaded because the user has left this page.
The asynchronous response is received and tries to update state for a component that is no longer loaded/mounted.
It's not entirely clear how this component is intended to fit into your overall structure, but you're going to need to change that structure. Either checking for authentication would need to be synchronous or you'd need to wait for the asynchronous operation to complete before redirecting. An example of the latter could be as simple as:
export function ProtectedRoute({ ...routeProps }: ProtectedRouteProps): ReactElement | null {
const context = useContext(AppContext);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
isUserAuthenticated(context.token)
.then(x => {
setIsAuthenticated(x);
setIsLoading(false);
})
.catch(e => {
setIsAuthenticated(false);
setIsLoading(false);
console.log(e);
});
});
if (isLoading) {
return <div>Loading...</div>;
} else if (isAuthenticated) {
return <Route {...routeProps} />;
} else {
return <Redirect to={{ pathname: "login" }} />;
}
}
In that scenario a separate state value of isLoading is used to track whether the asynchronous operation is still taking place, so the component "waits" until the data is loaded before deciding to redirect the user or not.
But overall I don't see why the authentication check can't be synchronous. Something higher-level, such as a provider component that wraps the entire application structure within <App/>, can have this same logic above, essentially performing the async operation and keeping the result in state. Then that state can be provided via useContext or Redux or even just passing as props to all child components.
You shouldn't need to re-check for authentication over and over in child components. That's an application-level concern.
You can use a variable to check component is mount or unmount when call setIsAuthenticated
useEffect(() => {
let isMouted = true;
isUserAuthenticated(context.token)
.then((val) => isMouted && setIsAuthenticated(val))
.catch(setIsAuthenticated);
return () => {
isMouted = false;
};
});
I have an endpoint in my app called /get-redirect, which basically redirects you to wherever you need to be.
E.g. if you're not logged in, the response from this endpoint is an HTTP 200 with the redirect_location field in the response set to mysite.com/login. If you're logged in, but haven't completed step 2 of onboarding, it sends an HTTP 200 with redirect_location set to mysite.com/step2, etc.
I want to use PrivateRoute in React Router for authenticated pages, and I want any redirects to go to the result of the /get-redirect endpoint. I would then render the appropriate component in the statement.
This is what I have so far but I keep getting that the getPage() function is returning undefined. What am I missing here?
const fakeAuth = {
getPage(cb) {
if (document.cookie === null) {
return '/login'
}
const url = "https://api.mysite.com/get-redirect"
axios.get(url)
.then(function (response) {
return response.redirect_location
}).catch(function (error) {
if (error.response.status === 401) {
return '/401'
}
return '/404'
})
},
}
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => {
return <Redirect to={{
pathname: fakeAuth.getPage()
}} />
}} />
)
}
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Router>
<Switch>
...
<PrivateRoute path="/dashboard">
<Dashboard />
</PrivateRoute>
....
Issue
getPage needs to return the Promise chain started by axios.
You will need to wait for the Promise to resolve (or reject) in the PrivateRoute component but you can't just return the result as the redirect target.
Solution
I suggest abstracting a component to do the endpoint check and conditionally render the redirect or the original children prop. While the endpoint is checked conditionally render some loading state or null until the expected result is returned then conditionally render the redirect or child Dashboard component.
Example
const CheckRedirect = ({ children }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState("/");
React.useEffect(() => {
fakeAuth
.getPage()
.then((target) => {
setTarget(target);
})
.finally(() => setIsChecking(false));
}, []);
if (isChecking) return "...Checking"; // or loading spinner, or null
return target.redirect_location ? (
<Redirect to={target.redirect_location} />
) : (
children
);
};
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} />} />
);
}
Demo
For demo this is the mocked auth getPage component, it has a 50% chance to return redirect target.
const fakeAuth = {
getPage(cb) {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(
Math.random() < 0.5 ? { redirect_location: "/step2" } : {}
);
}, 3000);
});
}
};
I am getting a warning in my web app and have read a lot of posts about the issue. Unfortunately I have not managed to resolve my problem and hope someone might have some advice. From what I can tell I need to find a way of dispatching to the store in a useEffect. But my efforts so far have been unsuccessful.
The warning says:
index.js:1 Warning: Cannot update a component (Connect(TabMenuComponent)) while rendering a different component (ReactRedux.Consumer). To locate the bad setState() call inside ReactRedux.Consumer, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
The stack trace further points me to this file. It points to line 30 which is the store.dispatch line:
export const AuthRoute = ({ component: Component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
store.dispatch({ type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <Component {...props} />;
}}
</ReactReduxContext.Consumer>;
}
} />
);
You are dispatching an action in the middle of a render which is not correct. What you should instead do is to create an HOC or a wrapper component to your input Component and dispatch the action once thee component is mounted
With class component wrapper:
class CompWithDispatch extends React.Component {
componentDidMount() {
const { store, type, url, path, params } = this.props;
store.dispatch({ type, url, path, params })
}
render() {
const { store, type, url, path, params , component: Component, ...rest} = this.props;
return <Component {...rest} />
}
}
With function component wrapper
const CompWithDispatch = (props) => {
const { store, type, url, path, params, component:Component, ...rest } = props;
useEffect(() => {
store.dispatch({ type, url, path, params })
}, []);
return <Component {...rest} />
}
and use it like
export const AuthRoute = ({ component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
const additionalProps = { type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <CompWithDispatch {...additionalProps} {...props} component={component}/>;
}}
</ReactReduxContext.Consumer>;
}
} />
);
In my app, I have a custom protected route that renders content dynamically based on isAuthenticated variable shown below.
return (
<Route {...rest} render={() => {
return isAuthenticated === true ? children : <Redirect to='/login' />
}} />
)
The following code is above this:
const [isAuthenticated, setIsAuthenticated] = useState(null)
useEffect(() => {
const isAuth = async () => {
try {
const result = await axios.get(`${BASE_URL}/api/auth/isauth`, {headers: {'x-auth-token': localStorage.getItem("userJWT")}})
setIsAuthenticated(true)
}
catch (err) {
if (err) setIsAuthenticated(false)
}
}
isAuth()
}, [])
When the user goes to this protected route, the useState is set to null by default, however this causes a redirect even if the user is authenticated. How do I set the state initally to the result of the isAuth function (true or false) in the useEffect instead of a value of null? Thanks.