React Router *Not Found* rendered first before main routes - reactjs

i am working on a huge project and i have an authorization system where Logged in users can access certain routes while the Not Found component, served on Logged Out Users. The problem is, when a user is marked as logged in and has a valid token and then proceed into a page (e.g /account), the component Not Found is rendered first and a second later the component account is rendered correctly.
I have an async componentDidMount where i validate my token and if it's valid, i set the state of isLogged: true. Also on each route in my switch i am checking the the state of isLogged and only then i send the corresponding component.
My async componentDidMount
{/* Username is from localstorage */}
async componentDidMount() {
if ((USERNAME != "") && (USERNAME != null)) {
const logged = await checkTheToken({'Username': USERNAME}).then((result) => {
if(result.status == 202) {
this.setState({isLogged: true});
this.setState({checking: false});
};
});
}
}
This is the Router (* supposing we are trying to access /user page. Notice i use react-router, react-router-dom ^4.1.1* )
if (this.state.checking) {
return null;
} else {
return (
<Router>
<Switch>
{/* This is Not Rendered First and Not Found rendered before FALSLY */}
{this.state.isLogged && <Route exact path="/user" component={() => (<User isLogged={this.state.isLogged} username={USERNAME} />)} />}
{/* This is Rendered First and Not Found is not rendering at all CORRECTLY */}
<Route exact path="/test" component={() => (<Test isLogged={this.state.isLogged} />)} />
<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />
</Switch>
</Router>
);
I think the problem is in the {this.state.isLogged && ...} because if we try to access /test the component is rendered correctly without Not Found rendering first.
Also, I tested all the lifecycle methods

I think you're right and the issue comes from {this.state.isLogged && ...}
Lets take it step by step.
First this.state.isLogged is falsy. It does mean that <User isLogged={this.state.isLogged} username={USERNAME} /> is not currently in the ReactRouter configuration.
We can guess that ReactRouter will match the default component (<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />) since /user is not in its configuration.
The actual behavior is then correct.
The fastest way to archieve your goal would be to move the token check into your child component with something like this :
async componentDidMount() {
if ((USERNAME != "") && (USERNAME != null)) {
const logged = await checkTheToken({'Username': USERNAME}).then((result) => {
if(result.status !== 202) {
history.push('/not-found')
} else {
this.setState({isLogged: true});
this.setState({checking: false});
};
});
}
}
Rendering function would like this :
render () {
if(!this.state.isLogged) return <span />
return <div>...</div>
}
The main problem with this aproach is that it would require all your authenticated components to implement this.
You'll also need to factorize the code into a service to avoid multiple calls.
A 2nd approach would be to fatorize this into a proxy component that do the check like this :
<Router>
<Switch>
<Route component={Authenticated}>
<Route exact path="/user" component={() => (<User isLogged={this.state.isLogged} username={USERNAME} />)} />
</Route>
<Route exact path="/test" component={() => (<Test isLogged={this.state.isLogged} />)} />
<Route component={() => (<NotFound isLogged={this.state.isLogged} />)} />
</Switch>
</Router>
This component will now be the one who carry the token check.
Rendering function would look like this :
render () {
if(!this.state.isLogged) return <span />
return {this.props.children}
}
Main problem here is that you can't easily share between your component the "this.state.isLogged"
If you want to share and update mutiple component using some global state, i highly suggest you to give a look at Redux.
Since redux can have a hard learning curve, if you only need to share this single value, you could try some things using the observable pattern. (Example : observable service, observable service + connector )
All my code example are not tested and are here to guide you in the right direction. In my opinion there is a lot of way to archieve what you want, sadly i can only tell you why it currently do not work and i hope i gave you enough clues to find a solution adapted to your use case.

Related

On React Router, stay on the same page even if refreshed

my site is built using MERN stack, when I refresh a page it first shows the main page and then the page where the user is. How to fix this issue?
For example:
if I refresh (/profile) page then for a meanwhile it shows (/) then it redirects to (/profile). I want if I refresh (/profile) it should be on the same page.
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({ component: Component, authed, ...rest }) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
Router code:
const App = () => {
const user = useSelector((state) => state?.auth);
return (
<>
<BrowserRouter>
<Container maxWidth="lg">
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" exact component={About} />
<Route path="/terms" exact component={Terms} />
<PrivateRoute authed={user?.authenticated} path='/profile' component={Profile} />
</Switch>
</Container>
</BrowserRouter>
</>
)
}
export default App;
How to fix so that user stays on the same page if its refreshed? The issue is on the pages where authentication is required.
When first authenticated the user, store the credentials(the info that you evaluate to see if the user is authenticated. Tokens etc.) in the localStorage. Of course you have to create necessary states too.
Then with useEffect hook on every render set the credential state from localStorage.
function YourComponentOrContext(){
const[credentials, setCredentials] = useState(null);
function yourLoginFunction(){
// Get credentials from backend response as response
setCredentials(response);
localStorage.setItem("credentials", response);
}
useEffect(() => {
let storedCredentials = localStorage.getItem("credentials");
if(!storedCredentials) return;
setCredentials(storedCredentials);
});
}
I guess on mounting (=first render) your user variable is empty. Then something asynchronous happen and you receive a new value for it, which leads to new evaluation of {user?.authenticated} resulting in true and causing a redirect to your /profile page.
I must say I'm not familiar with Redux (I see useSelector in your code, so I assume you are using a Redux store), but if you want to avoid such behaviour you need to retrieve the right user value on mounting OR only render route components when you've got it later.

How to use ProtectedRoute (RequireAuth) with fetch(url, method) in React (React Router v6)?

I want to redirect users to Unprotected (login/ register) components if the user is not logged in (or verified) else allow access to Protected components.
To achieve that, I tried to use,
ProtectedRoute techniques
PrivateRoute - failed to implement appropriately
RequiresLogin - This helped me to reach to the next approach
And some YouTube videos and articles found from Google
Code:
index.js
...
<Router>
<App />
</Router>
...
App.js - using the ProtectedRoute
...
<Routes>
<ProtectedRoute exact path={"page1"} element={<Page1 />}/>
<ProtectedRoute exact path={"page2"} element={<Page2 />}/>
<Route exact path={"login"} element={<Login />}/>
<Routes/>
...
RequireAuth
It seemed that it is a better approach then ProtectedRoute,
RequireAuth - works except, it is ProtectiveOverflow at the moment
Code:
index.js
// Unchanged
App.js
...
<Routes>
<Route exact path={"page1"} element={
<RequireAuth>
<Page1 />
</RequireAuth>
}/>
<Route exact path={"page2"} element={
<RequireAuth>
<Page2 />
</RequireAuth>
}/>
<Route exact path={"login"} element={<Login />}/>
</Routes>
...
It seemed to work and I was able to protect the protected components. After implementing the authorization process, which I am doing by sending a fetch(...) request to my API (djangorestframework) and getting the result dynamically everytime, I figured out that the protected components got a bit more protective than required. Now, although the server authenticating the request and sending sucessfull response, the protected pages are still locked.
Digging up, I realized that I have used the fetch function which is a Promise and it runs on a separate thread. So, before the fetch could return the result, my component already gets rendered and unmounted.
Code:
RequireAuth.js
...
function RequireAuth({children}) {
const [auth, setAuth] = useState(false);
fetch(urls.auth, methods.get())
.then(r => r.json())
.then(data => {
setAuth(data)
})
.catch(error => {
console.log(error)
});
return auth? children : <Navigate to={"../login"}/>;
}
...
I have gone through various technique to solve this, for example,
Using statefull component
Code:
RequireAuth.js
...
class RequireAuth extends React.Component {
constructor(props) {
super(props);
this.state = {auth: false}
fetch(urls.auth, methods.get())
.then(r => r.json())
.then(data => {
this.setState({auth: data})
})
.catch(error => {
console.log(error)
});
}
render() {
return this.state.auth? this.props.children : <Navigate to={"../login"}/>;
}
}
...
However, that failed too. Then I looked into,
Fetch data and then render it to dom React
Web Fetch API (waiting the fetch to complete and then executed the next instruction)
Trying to implement a SIMPLE promise in Reactjs
How to return data from promise
How to get data returned from fetch() promise?
How to finish all fetch before executing next function in React?
Finally,
I looked if I can do it using fetching or HttpRequest methods that does not run on different thread, but I afraid, even if it's possible, it can result in bad user experience. Therefore, please help me to solve this fetch authentication issue in React. And, I would also like if there were other ways to implement the dynamic authorization using React and djangorestframework that could do this protective page stuffs more efficiently.
Thank you.
Found this answer on StackOverflow as I could not stop searching just because I asked a question. This is very exciting and sad at the same time. I missed one very small part, I thought maybe it was not a very big deal. But guess what?
null is important
Yes, null is what I was missing while returning in my code. So, what I figured is if null is returned, react won't consider it as rendered and the render request will be keep asking, that's what I beleive. So I can simple set the default state of auth to be null and return null if it is null, else check the value of auth.
Code:
index.js
// Unchanged
...
<Router>
<App />
</Router>
...
App.js
// Unchanged
...
<Routes>
<Route exact path={"page1"} element={
<RequireAuth>
<Page1 />
</RequireAuth>
}/>
<Route exact path={"page2"} element={
<RequireAuth>
<Page2 />
</RequireAuth>
}/>
<Route exact path={"login"} element={<Login />}/>
</Routes>
...
RequireAuth.js - this is interesting
...
// Assumption: The server is sending `true` or `false` as the response for authentication for simplicity
function RequireAuth({children}) {
const [auth, setAuth] = useState(null); // The spell for the magic
fetch(urls.auth, methods.get())
.then(r => r.json())
.then(data => setAuth(data))
.catch(error => console.log(error));
return auth == null? // The code that did the magic
null : (auth?
children : <Navigate to={"/login"}/>) // Notice, used '/login' instead of 'login' to directly go to login/ authenticate page which is at the root path.
...

How do I achieve conditional routing based on the parameter in the requested route?

I am developing a React js blog-like web site and I am finding some trouble managing the routes. My pages with articles' URLs are like this: website/pages/3
I would like to redirect to homepage when the page index is 1, since the homepage is the first page with articles by default.
My Router looks like this:
<Router>
<Switch>
<Route exact path="/" render={() => <Page postsPerPage={3}/>} />
<Route exact path="/Page/:activePage" render={() => <Page postsPerPage={3}/>} />
<Route path="/Login" component={Login} />
<PrivateRoute path="/AddPost" component={AddPost} />
<Route path="/:postLocation" component={Post} />
</Switch>
</Router>
I would like to route "/Page/:activePage" to the component the route "/" renders if the activePage is 1. So the component would be the same (Page), but the path would be different.
Could conditional rendering in the Router do the trick, and if so, how? I was thinking about something along these lines:
<Route exact path="/Page/:activePage" render={() =>
{
let {activePage} = useParams()
if (activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}/>
However it seems React is not happy about me using useParams there (there's a compilation error: React Hook "useParams" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks)
I tested that snippet with a constant value of 1 instead of the activePage parameter and it does redirect so that basically leaves me with the question of how do I retrieve the parameter from the path?
Render function in this case The render prop function has access to all the same route props (match, location and history) as the component render prop.
so you can basically do something like.
<Route exact path="/Page/:activePage" render={(props) =>
{
if (props.match.params.activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}
/>
Looking at your example case above, I will rather not redirect anything but carry the logic into the Page component. inside the componentDidMount or useEffect function that extracts the activePage param. I will check if it's 1(or whatever sentinal value you choose to use) Then I perform the logic that will have been performed by the home component else I proceed normally with the route. eg If you extracted it and do a fetch to the backend for the case where its, not 1, then when it's 1 you could just return from the function and it will work as if it were on the home page. alternatively, after the check, if it's 1 you could then redirect back to '/'.
You should probably handle the routing within your Pages component or if you prefer, create a separate component to handle the conditional routing.
for example:
function Pages (props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : (
<div>
Your Pages component content here
</div>
)
}
export default Pages;
or
function ConditionalRoutingComponent(props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : <Page postsPerPage={3}/>
}
export default ConditionalRoutingComponent;
Hope this helps
The render function of Component the is called with three parameters namely match, history and location. You can use them to perform the action you are trying to do with hooks.
<Route ... render={({match}) => {
if (match.params.activePage == 1) {
doYourStuff()
}
}}

How to save bad invalid URLs that were typed in?

I am having a react-redux app and react-router v4 inside of app
Is there a way to catch all invalid URLs that were entered and save them to an array, like so ['https://mysite.co/progects', 'https://mysite.co/sometypo', 'https://mysite.co/something']?
And then I want to send that data to server for building some redirects and some sitemap
Currently I have this:
<Switch>
{/* <Route path='/blog' exact component={Blog} /> */}
<Route path='/projects/:id' component={ProjectDetails} />
<Route path='/career/:id' component={CareerDetails} />
<Route path='/apply-for-job' render={(props) => (
<ModalWindow
{...props}
modalHeader='Apply form'>
<ApplyForm history={props.history} />
</ModalWindow>
)} />
<Route exact path='/' component={withScrollPreservation(LandingPage)} />
<Route component={NoMatch} />
{/* <Route component={withScrollPreservation(LandingPage)} /> */}
</Switch>
In your NoMatch component, you can have the logic to update unmatched/incorrect urls
class NoMatch extends React.Component {
componentDidMount() {
const { addNoMatchUrl } = this.props;
// you might want to handle the duplicate url logic here too in addNoMatchUrl method
addNoMatchUrl(this.props.location.pathname);
}
componentDidUpdate(prevProps) {
const { location, addNoMatchUrl } = this.props;
if (location.pathname !== prevProps.location.pathname) {
addNoMatchUrl(location.pathname);
}
}
render() {
// render logic
}
}
export default connect(null, {addNoMatchUrl});
If you want, say, to redirect someone, who typed '/progects' to '/projects' - well, that's nice UX, but your Switch block will be cluttered with tens of possible invalid urls.
As I see it, maybe you should add <Redirect to='/main' /> at the bottom of your Switch so any invalid url gets redirected to Main component (or whichever you have) or to 404-Component.
If you still want to gather them, then instead of redirecting to Main or 404 Component, send them to specific Error component, where you can get the link via this.props.history.location and handle that link further in the component: send to server, set that url in local/session storage, etc.
Note that you'll need a way to store that data someplace which won't get cleared on unmounting.
In order to send users to that route you'll need to place that at the bottom of your Switch.
<Switch>
...{your routes}
<Route component={Error} />
</Switch>
So actually all you need to do is handle those urls in your NoMatch component, like so
const path = this.props.history.location;
axios.post('your api', path);

How to get params in parent route component

I'm building an app with React, React-Router (v5) and Redux and wonder how to access the params of the current URL in a parent route.
That's my entry, the router.js:
<Wrapper>
<Route exact path="/login" render={(props) => (
<LoginPage {...props} entryPath={this.entryPath} />
)} />
<Route exact path="/" component={UserIsAuthenticated(HomePage)} />
<Route exact path="/about" component={UserIsAuthenticated(AboutPage)} />
<Route path="/projects" component={UserIsAuthenticated(ProjectsPage)} />
</Wrapper>
And that's my ProjectsPage component:
class ProjectsPage extends PureComponent {
componentDidMount() {
this.props.fetchProjectsRequest()
}
render() {
console.log(this.props.active)
if (this.props.loading) {
return <Loading />
} else {
return (
<Wrapper>
<Sidebar>
<ProjectList projects={this.props.projects} />
</Sidebar>
<Content>
<Route exact path="/projects" component={ProjectsDashboard} />
<Switch>
<Route exact path="/projects/new" component={ProjectNew} />
<Route exact path="/projects/:id/edit" component={ProjectEdit} />
<Route exact path="/projects/:id" component={ProjectPage} />
</Switch>
</Content>
</Wrapper>
)
}
}
}
const enhance = connect(
(state, props) => ({
active: props.match,
loading: projectSelectors.loading(state),
projects: projectSelectors.projects(state)
}),
{
fetchProjectsRequest
}
)
export default withRouter(enhance(ProjectsPage))
The problem is, that the console.log output in my render method is {"path":"/projects","url":"/projects","isExact":false,"params":{}} although the URL is http://localhost:3000/projects/14.
I want to add an ID prop to my ProjectList to highlight the currently selected project.
I could save the ID of the project in a store inside my ProjectPage component, but I think this would be a bit confusing, especially because the URL has the information actually – so why should I write something in the store?
Another (bad?) approach would be to parse the location object the get the ID by myself, but I think there is a react-router/react-router-redux way to get the params at this point that I've overlooked.
#Kyle explained issue very well from technical perspective.
I will just focus on solution to that problem.
You can use matchPath to get id of selected project.
matchPath - https://reacttraining.com/react-router/web/api/matchPath
This lets you use the same matching code that uses except
outside of the normal render cycle, like gathering up data
dependencies before rendering on the server.
Usage in this case is very straight forward.
1 Use matchPath
// history is one of the props passed by react-router to component
// #link https://reacttraining.com/react-router/web/api/history
const match = matchPath(history.location.pathname, {
// You can share this string as a constant if you want
path: "/articles/:id"
});
let articleId;
// match can be null
if (match && match.params.id) {
articleId = match.params.id;
}
2 Use articleId in render
{articleId && (
<h1>You selected article with id: {articleId}</h1>
)}
I build a simple demo which you can use to implement the same functionality in your project.
Demo: https://codesandbox.io/s/pQo6YMZop
I think that this solution is quite elegant because we use official react-router API which is also used for path matching in router. We also don't use window.location here so testing / mocking will be easy if you export also raw component.
TLDR
React router match.params will be an empty object if your <Route /> path property doesn't include :params.
Solve this use case: You could use let id = window.location.pathname.split("/").pop(); in your parent route's component to get the id.
Detailed reason why not
If the path prop supplied to a <Route /> doesn't have any params, such as /:id, react router isn't going to doing the parsing for you. If you look in matchPath.js at line #56 you can start to see how the match prop is constructed.
return {
path: path, // the path pattern used to match
url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
isExact: isExact, // whether or not we matched exactly
params: keys.reduce(function (memo, key, index) {
memo[key.name] = values[index];
return memo;
}, {})
};
Then we can look at line #43 you can see that keys comes from _compilePath.keys. We can then look at the compilePath function and see that it uses pathToRegexp(), which will use stringToRegexp(), which will use tokensToRegExp() which will then mutate keys on line #355 with keys.push(token). With a <Route /> that has no params value in its path prop, the parse() function used in stringToRegexp() will not return any tokens and line #355 won't even be reached because the only tokens array will not contain any token objects.
So.... if your <Route /> doesn't have :params then the keys value will be an empty array and you will not have any params on match.
In conclusion, it looks like you're going to have to get the params yourself if your route isn't taking them into account. You could to that by using let id = window.location.pathname.split("/").pop().

Resources