so I'm using Redux-Toolkit Query on my project, and I have an authSlice, where I keep the authenticated user info and an access_token.
I also keep this info in local storage so whenever I reload the page I can get the values from the local storage and save them in the state.
The catch is that I have a RequiredAuth component that checks if the user trying to access specific routes is authenticated, by checking if there is an access_token in the state, it works fine except that if I reload this page while I'm authenticated I will be redirected to my login page.
The RequiredAuth component code:
import { useLocation, Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectToken } from "./authSlice";
const RequireAuth = () => {
const token = useSelector(selectToken)
const location = useLocation()
return (
token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
Code that gets user info and token from local storage when the page is reloaded and adds it to state:
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setCredentials, selectToken } from '../features/auth/authSlice';
const Header = () => {
const dispatch = useDispatch()
const [user, setUser] = useState(JSON.parse(localStorage.getItem('profile')))
const stateToken = useSelector(selectToken)
useEffect(() => {
if(user?.access_token && !stateToken) {
dispatch(setCredentials({ user: user.user, access_token: user.access_token }))
}
}, [])
// Omited code, not relevant
return (
<header className='nav-bar'>
// Omited code, not relevant
</header>
)
}
export default Header
I believe whenever I reload a page where a user needs to be authenticated this happens: in the "RequiredAuth" I will get a null access_token from the state, so I get redirected and only then my useEffect will copy the local storage data to the state.
I fixed this problem by changing the RequiredAuth component to this:
import { useLocation, Navigate, Outlet } from "react-router-dom";
const RequireAuth = () => {
const profile = JSON.parse(localStorage.getItem('profile'))
const location = useLocation()
return (
profile?.access_token ? <Outlet /> : <Navigate to="/auth/login" state={{ from:
location}} replace />
)
}
export default RequireAuth
But I would like to know if there is a better way to keep data in state after reloading a page in order to solve this problem because getting it from local storage feels counterintuitive, since the data will be stored in the state after the useEffect logic completes.
I think this redux-persist package can help. It basically stores your state inside localStorage/sessionStorage and it will rehidrate your state every time you refresh the page.
You can't keep data with a refresh. Netherless, you can use redux-persist. This package keeps the data inside the localstorage and populate automatically the store on a refresh.
you have to use redux-persist with rtk for the store token. above method, if u r navigating to other routes it will work fine. suppose u r refresh the page u will redirect to the login page
Refer redux-persist
Related
Is there a built-in way in react-router-dom v6, to go back to the previous page, BUT in case the previous page is out of the context of the application, to route to the root and to thus not out of the application.
Example: I surf to a www.thing.com/thingy from www.google.com, this page (www.thing.com/thingy) has a go back button on it => when I click on the go back button => I am redirected to www.google.com instead of the wanted behaviour a redirect to www.thing.com.
Mockup of an example page.
I have tried several implementations and searched through the documentation but couldn't find a built-in way to resolve this. As far as I can see there isn't a way. I can however make something custom to resolve my issue if its not.
import { useNavigate } from 'react-router-dom';
function YourApp() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>go back</button>
</>
);
}
I solved it by keeping track of the history.
If a user had not yet been on the page, I redirect them to the homepage.
Else redirect them to the previous page.
import {
useEffect
} from 'react';
import {
createContext,
useMemo,
useState
} from 'react';
import {
useLocation
} from 'react-router-dom';
export const LocationHistoryContext = createContext({});
const LocationHistoryProvider = ({
children
}) => {
const [locationHistory, setLocationHistory] = useState(
new Set(),
);
const location = useLocation();
useEffect(() => {
// if pathname has changed, add it to the history
let path = location.pathname.match(/^\/([^/])*/)[0];
setLocationHistory((prev) => new Set([path, ...prev]));
}, [location.pathname, location]);
const context = useMemo(() => {
return {
/* if the user has visited more than one page */
hasHistory: locationHistory.size > 1,
};
}, [locationHistory]);
return ( <
LocationHistoryContext.Provider value = {context}>
{
children
}
</LocationHistoryContext.Provider>
);
};
export default LocationHistoryProvider;
This question already has answers here:
How to pass data from a page to another page using react router
(5 answers)
Closed 8 months ago.
I am using React Router v6 in my application. The user should be redirected to the login page when not logged in and an error message should be shown. So far the redirection part is working well and it takes the user to the login page. I need to show the authentication required message in the login page.
The code is as follows:
const PrivateRoute = ({ Component }) => {
const auth = false; //your logic
return auth ? (
<Component />
) : (
<Navigate
to="/login"
replace={true}
state={{ alert: "Authentication is required" }}
/>
);
};
The login component is as follows:
function Login(props) {
debugger
return <h1>Login page</h1>;
}
How do I access the error message in the login component? Something like this.props.alert ?
You can access it like this:
import { useLocation } from "react-router-dom";
Then:
const { state } = useLocation();
return <div>{state.alert}</div>;
You could access the alert message with the help of useLocation from React Router Dom. Like this:
import { useLocation } from "react-router-dom"
function Login(props) {
const location = useLocation();
return <h1>{location.state.alert}</h1>;
}
I want the logout button to show up only when the user's logged in. It kind of works now but it's not working the way it should.
With the code below, the logout button shows up when the user's logged in only if the page's refreshed. That's because upon the <App/> component being loaded, the <Navbar/> component mounted along with it.
But how can I make it so that even if <Navbar/>'s loaded, it can still be possible to manipulate when the button can appear based on if the auth token is not null?
Here's App.js:
const App = () => {
let [logoutButtonFlag, setLogoutButtonFlag] = useState(false);
let authToken = localStorage.getItem('token');
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
return (
<>
<Navbar logoutButtonFlag={logoutButtonFlag}/>
</>
);
}
export default App;
Here's Navbar.js:
const Navbar = (props) => {
return (
{!props.logoutButtonFlag ? null : <button className="learn-more">Logout</button>}
);
};
export default Navbar;
you are providing a non-state variable to the list of states that useEffect hook 'listen' to, so it will not run again after you change its value.
const [authToken, setAuthToken] = useState(localStorage.getItem('token'));
and when you update the local storge "token" also update authToken to the same value.
and your useEffect will retrigger on authToken change because now its a state
useEffect(() => {
if (authToken !== null) {
setLogoutButtonFlag(true);
}
}, [authToken]);
The reason way only on refresh it was updating is because the value of the "token" in local storage was changed already.
In my app, I am using react-router v5 and react/typescript I have a component that uses the react-query and fetches some data. At the moment it only fetches the data when the component is rendered the first time, When navigating the request does not get cancelled and navigating back it does not make a new request. This component takes in an id parameter which fetches the data based on the id, so it needs to either refresh the component or maybe I need to add the method into the useEffect hook?
Routing component
import React from 'react';
import { BrowserRouter, Route, Switch} from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RouteComponentProps } from "react-router-dom";
import Component1 from '../Component1';
import Component2 from '../Component2';
const queryClient = new QueryClient()
const Routing: React.FunctionComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Switch>
<Route exact path="/" component={Component1} />
<Route path="/details/:id" render={(props: RouteComponentProps<any>) => <Component2 {...props}/>} />
<Route component={NotFound} />
</Switch>
</BrowserRouter>
</QueryClientProvider>
)
}
export default Routing;
Component2 (id)
import React from 'react';
import { useQuery } from 'react-query';
import { RouteComponentProps, useLocation } from "react-router-dom";
interface stateType {
model: { pathname: string },
start: { pathname: string | Date }
}
const Component2: React.FunctionComponent<RouteComponentProps<any>> = (props) => {
const { state } = useLocation<stateType>();
let alertInnerId = props.match.params.id;
const fetchChart = async () => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const { data, status } = useQuery('planeInfo', fetchPlane, {
staleTime: 5000,
});
return (
<>
{status === 'error' && (
<div className="mt-5">Error fetching data!</div>
)}
{status === 'loading' && (
<div className="mt-5">Loading data ...
</div>
)}
{status === 'success' && (
{data.map(inner => {
return (
<p>{inner.id}</p>
)
})}
)}
</div>
</>
)
}
export default Component2;
In the Component 1 I am programmatically navigating:
onClick={() => history.push(`/detail/${id}}`, { model: plane.model, start: formattedStartDateTime })}>
Either way by programmatically or normal, its still the same.
[...] and navigating back it does not make a new request.
First of all, according to your code, as per the staleTime option that is set as an option on useQuery itself, the cache should invalidate every five seconds. So each time the useQuery hook is mounted (such as on route change), if five seconds have passed, a new request should be made. Your code does appear to be incomplete though as you're referencing id which appears to be undefined.
In any case, since you are requesting details of a resource with an ID, you should consider using a query key like: [planeInfo, id] instead of planeInfo alone. From the documentation:
Since query keys uniquely describe the data they are fetching, they
should include any variables you use in your query function that
change. For example:
function Todos({ todoId }) {
const result = useQuery(['todos', todoId], () =>
fetchTodoById(todoId))
}
To handle canceling the request on navigation:
You can't wrap the useQuery hook from React Query in a useEffect hook, but rather you can use use the return function of useEffect to clean up your useQuery request, effectively canceling the request when the component unmounts. With useQuery there are two ways (possibly more) to cancel a request:
use the remove method exposed on the returned object of useQuery
use the QueryClient method: cancelQueries
(see: useQuery reference here)
see: QueryClient reference here and specifically cancelQueries
Using remove with useEffect
(I've only kept the relevant bits of your code)
const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
const fetchChart = async() => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const {
data,
status,
/** access the remove method **/
remove
} = useQuery('planeInfo', fetchPlane, {
staleTime: 5000,
});
useEffect(() => {
/** when this component unmounts, call it **/
return () => remove()
/** make sure to set an empty deps array **/
}, [])
/** the rest of your component **/
}
Calling remove like this will cancel any ongoing request, but as its name suggests, it also removes the query from the cache. Depending on whether you need to keep the data in cache or not, this may or may not be a viable strategy. If you need to keep the data, you can instead use the canceQueries method.
Using cancelQueries with useEffect
Much like before except here you need to export your queryClient instance from the routing component file (as you have it defined there) and then you're importing that instance of QueryClient into Component2 and calling cancelQueries on the cache key from useEffect:
import { queryClient } from "./routing-component"
const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
const fetchChart = async() => {
const res = await fetch(`/detail/${id}`);
return res.json();
};
const {
data,
status,
} = useQuery(['planeInfo', id], fetchPlane, {
staleTime: 5000,
});
useEffect(() => {
/** when this component unmounts, call it **/
return () => queryClient.cancelQueries(['planeInfo', id], {exact: true, fetching: true})
}, [])
/** the rest of your component **/
}
Here you see that I've implemented the query key as I suggested before, with the id as well. You can see why having a more precise reference to the cached object can be beneficial. I'm also using two query filters: exact and fetching. Setting exact to true will make sure React Query doesn't use pattern matching and cancel a broader set of queries. You can decide whether or not this is necessary for your implementation needs. Setting fetching to true will make sure React Query includes and cancels and queries that are currently fetching data.
Just note that by depending on useEffect, it is in some cases possible for it's parent component to unmount due to factors other than the user navigating away from the page (such as a modal). In such cases, you should move your useQuery up in the component tree into a component that will only unmount when a user navigates, and then pass the result of useQuery into the child component as props, to avoid premature cancellations.
Alternatively you could use Axios instead of fetch. With Axios you can cancel a request using a global cancel token, and combine executing that cancellation with React Router's useLocation (example here). You could of course also combine useLocation listening to route changes with QueryClient.cancelQueries. There are in fact, many possible approaches to your question.
I am making a simple Next Js application which has only two pages..
index.tsx:
import React from "react";
import Link from "next/link";
export default function Index() {
return (
<div>
<Link
href={{
pathname: "/about",
query: { candidateId: 8432 }
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
As per the above code, on click Go to the about page it goes to about page and using query I also receive the passed query values in about page.
about.tsx
import React from "react";
import Router, { withRouter } from "next/router";
function About({ router: { query } }: any) {
return (
<div>
Candidate Id: <b> {query.candidateId} </b>
</div>
);
}
export default withRouter(About);
This displays the value but on page refresh while we are in /about page, the candidateId received gets disappeared.
Requirement: Kindly help me to retain the query value passed down from one page to another page even on page refresh.
Note: As per my requirement I should not display the canidateId on url while navigating and hence I am using as approach.. I know I can achieve it if I remove as but I cannot remove that here in index page while navigating.. Reason is this will lead to displaying candidateId in the url which is not intended..
Tried this solution: https://stackoverflow.com/a/62974489/7785337 but this gives empty query object on refresh of page.
Stuck for very long time with this please kindly help me.
If you do not want to use the query parameter you may need to create a "store" that saves your variable that persist throughout your pages.
Sample code as follows.
//candidatestore.js
export const CandidateStoreContext = createContext()
export const useCandidateStore = () => {
const context = useContext(CandidateStoreContext)
if (!context) {
throw new Error(`useStore must be used within a CandidateStoreContext`)
}
return context
}
export const CandidateStoreProvider = ({ children }) => {
const [candidateId, setCandidateId] = useState(null);
return (
<CandidateStoreContext.Provider value={{ candidateId, setCandidateId }}>
{children}
</CandidateStoreContext.Provider >
)
}
Then you need to wrap the Provider around your app like
<CandidateStoreProvider><App /></CandidateStoreProvider>
This way you can use anywhere as follows both in your index page and your about page.
const { candidateId, setCandidateId } = useCandidateStore()
UseContext
In your codes, it should probably look something like that.
import React from "react";
import Link from "next/link";
import { useCandidateStore } from './candidatestore'
export default function Index() {
const { candidateId, setCandidateId } = useCandidateStore()
useEffect(() => {
setCandidateId(thecandidateId)
})
return (
<div>
<Link
href={{
pathname: "/about",
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
function About({ router: { query } }: any) {
const { candidateId, setCandidateId } = useCandidateStore()
return (
<div>
Candidate Id: <b> {candidateId} </b>
</div>
);
}
Update to Next.JS 10. It comes with Automatic Resolving of href which fixes your problem.
Try to delete the as="about" and then navigate again to the "about" page, the issue should be gone.
Codesandbox
My best bet would be to store the candidateId in an encrypted session on the client side. You could read/verify cookies in getServerSideProps() and pass their contents to the page component. If this sounds feasible, I'd recommend checking out the next-iron-session.
Another approach would be to check if candidateId exists in the query object in getServerSideProps(). If it does then pass it straight to the page component. If not, either get it elsewhere, redirect, or pass some default value. Append the following starter code to your about.tsx:
/* ... */
export function getServerSideProps({ query }: any) {
// if query object was received, return it as a router prop:
if (query.candidateId) {
return { props: { router: { query } } };
}
// obtain candidateId elsewhere, redirect or fallback to some default value:
/* ... */
return { props: { router: { query: { candidateId: 8432 } } } };
}
index.tsx file
Keep the code same as it is.
import React from "react";
import Link from "next/link";
export default function Index() {
return (
<div>
<Link
href={{
pathname: "/about",
query: { candidateId: 8432 }
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
AboutUs.tsx
Code starts from here
Adding router as a dependency in the useEffect the issue should get solved.
import Router, { useRouter } from "next/router";
import React, { useState, useEffect } from 'react';
function About({ router: { query } }: any) {
const route = userRouter();
const [candidateId, setCandidateid] = useState();
useEffect(() => {
const {candidateId} = router.query
if(candidateId) {
setCandidateid(candidateid)
}},[router]) //Here goes the dependency
return (
<div>
Candidate Id: <b> {candidateId} </b>
</div>
);
}
export default (About);