I'm writing a react app that uses apollo-client
and I'm using apollo-link-error
to catch authentication errors globally. I'm using createBrowserHistory
for browser history manipulation and redux
to manage my app state.
On authentication error I want to redirect the user to the /login
page.
However, doing so with history.push('/login')
and forceRefresh:false changes the URL but doesn't actually navigate inside my app.
If I use forceRefresh:true
it works, but the app is completely restarted, which I'd like to avoid.
const errorLink = onError(({ graphQLErrors, networkError }) => {
if(graphQLErrors[0].extensions.code == "UNAUTHENTICATED") {
// with forceRefresh:true this works, but causes a nasty
// reload, without the app doesn't navigate, only the url changes
history.push('/login')
}
});
`
let links = [errorLink, authLink, httpLink];
const link = ApolloLink.from(links);
const client = new ApolloClient({
link: link,
cache: new InMemoryCache(),
connectToDevTools: true,
});
I think the problem is that I'm not using redux-router
methods to navigate (so the app stays the same even though the url changes)
Q: how do I get a redux history
object similar to using withRouter()
when I'm not inside a component? What is the proper way to handle this situation?
The react-apollo package has been deprecated, and the functionality offered by each of the above packages can now be accessed from @apollo/client directly: @apollo/react-hooks -> now available directly from @apollo/client.
graphQLErrors and the response data is set to undefined even if the server returns data in its response. This means network errors and GraphQL errors result in a similar response shape. This is the default error policy. ignore. graphQLErrors are ignored ( error.
To connect Apollo Client to React, you will need to use the ApolloProvider component exported from @apollo/react-hooks . The ApolloProvider is similar to React's Context. Provider . It wraps your React app and places the client on the context, which allows you to access it from anywhere in your component tree.
Short summary of one possible solution:
<ProtectedRoute>
component that redirects unauthenticated users to login page. ProtectedRoute-component just check if user has valid token, if not redirects user.location.reload()
Detailed implementation below.
I was not able to find any straightforward solution. In normal cases, to redirect user I use react-router navigate() hook. Inside the error link I found no way to use react-hooks.
However, I managed to solve the actual problem. I implemented ProtectedRoute component which wraps all the parts of the application which requires authentication:
type ProtectedRouteProps = {
path: string;
toRedirect: string;
};
export const ProtectedRoute: FunctionComponent<ProtectedRouteProps> = ({
path,
toRedirect,
children,
}) => {
return isAuthenticated() ? (
<Route path={path}>
{children}
</Route>
) : (
<Navigate to={{ pathname: toRedirect }} />
);
};
type ValidToken = string;
type ExpiredToken = 'Expired token'
type NullToken = '' | null
export type JwtTokenType = (ValidToken | ExpiredToken | NullToken )
export const isNullToken = (token: JwtTokenType) : boolean => {
return (token === '' || token === null)
}
export const isExpiredToken = (token: JwtTokenType) : boolean => {
return token === "Expired token"
}
export const isAuthenticated = () : boolean => {
let token = getTokenFromCookies();
return !(isExpiredToken(token) || isNullToken(token));
}
I use it as follows:
<Routes>
<Route path="login" element={<LoginPage />} />
<ProtectedRoute path="/*" toRedirect="login">
// protected routes here
</ProtectedRoute>
</Routes>
To handle logouts and redirects for unauthenticated user I implemented two functions:
// Use this in normal cases
export function useHandleLogout(): () => void {
const navigate = useNavigate();
// maybe call other hooks
});
function handleLogout() {
navigate("/login");
removeToken();
// do other stuff you want
}
return handleLogout;
}
// Use this inside error-link
export const handleLogoutWithoutHook = () => {
// Logout without hook
removeToken();
// do other stuff required when logout
// eslint-disable-next-line no-restricted-globals
location.reload();
// location.reload() after token removed affects user redirect
// when component is wrapped inside <ProtectedRoute> component
};
export const removeToken = () => {
Cookies.remove("jwt-token")
}
And finally inside the error link:
export const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (let err of graphQLErrors) {
if (err.message.includes('AnonymousUser')) {
handleLogoutWithoutHook()
return
}
if (err.message.includes('Signature has expired')) {
handleLogoutWithoutHook()
}
console.log(err.message)
}
}
return forward(operation)
}
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With