Disable route unless (condition) - React, Routing - reactjs

The Issue:
I am facing an issue routing to a nested route (ExamResult component), I'd like to disable any kind of navigation to this specific route unless I've submitted a form.
Demo video:
https://www.screencast.com/t/cayuOnsa8
Code:
App.js Routes :
<Routes>
...
<Route path='exams/:id' element={<ExamPage />} >
<Route path='result' element={<ExamResult />} />
</Route>
<Route path='exams/:id/add-question' element={<RequireAuth><AddQuestion /></RequireAuth>}/>
{/* <Route path='exams/:id/result' element={<ExamResult />} /> */}
...
<Route path='*' element={<NoMatch />} />
</Routes>
QuestionList.js Component - Navigation to result route after quiz submission:
navigate('result', { state });
Repo: quiz-react-storybook
Github Open Issue: issue
Expectations:
Only after I'm submitting the quiz I would expect the navigation to work, otherwise navigating to exams/:id/result path won't work.

The ExamPage component must render an Outlet component for the nested routes to rendered. You could conditionally render the Outlet based on whether or not the form has been submitted.
...
{formSubmitted && <Outlet />}
...
An alternative method might be to pass some route state along with the route transition to the "exams/:id/result" route and check in ExamResult whether or not the navigation was valid and issue a back navigation if it wasn't.
navigate(
'result',
{
state: {
...state,
formSubmitted: true,
}
}
);
ExamResult
const navigate = useNavigate();
const { state } = useLocation();
useEffect(() => {
if (!state?.formSubmitted) {
navigate(-1);
}
}, []);
...

manage global state(ex. - Submitted: false) in context or redux to check whether the form is submitted or not. set this state to true on submit.
in result component based on condition return result's JSX or navigate to home if Submitted = false.

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.

Why isn't my react app redirecting to home after login?

I'm using react-router-dom to help handle routing but when I try to switch to the "/" route (my home page) after the user's auth state changes, it does not trigger the redirect. Routes work otherwise throughout the app (via Links). The app is properly reflecting the user state throughout, just not redirecting.
My SignUp component is a class not functional component so I can't use hooks such as useHistory.
Here's my App.tsx:
componentDidMount() {
auth.onAuthStateChanged((user) => {
this.setState({ currentUser: user }, () => {
console.log("this is running")
// up to here is in fact being called when there is a successful sign in
return <Redirect to="/" />;
});
});
}
render() {
return (
<div>
<Header currentUser={this.state.currentUser} />
<Switch>
<Route path="/" exact={true}>
<Home currentUser={this.state.currentUser} key={`home`} />
</Route>
<Route
exact={true}
path="/sign-up"
component={SignUp}
key={`sign-up`}
/>
</Switch>
</div>
);
}
}
try to use this:
<Redirect
to={{
pathname: "/",
state: { from: location }
}}
/>
I'm more into functional components, but you're returning a tag on auth change, right?
Is this doing anything?
I think the <Redirect> tag only works in a <Router> after the route has been triggered.
Maybe you can find something here: How to push to History in React Router v4?

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 programatically go one level up of the route hierarchy with React router 5?

I have one functional requirement in my project that it should have a back button which will navigate user to one level up of the route hierarchy.
Consider I have following routes:
// Home Component
<div>
<Route path="/courses" component={CourseComponent} />
<Route path="/settings" component={SettingsComponent} />
</div>
Nested routes:
// Course Component
<Switch>
<Route path={`${match.path}/:courseId/details`} component={CourseDetailComponent} />
<Route path={`${match.path}/classes`} component={ClassComponent} />
<Route
exact
path={match.path}
render={() => <h3>Courses...</h3>}
/>
</Switch>
Now I want to navigate user from CourseDetailComponent to CourseComponent using that back button.
Note: I can't use history since this will use browser history considering the case: if user directly land on a page using a URL.
You have to use one component in your App, which knows all the changes. So, you can have a parent App component, which will be notified from all changes
<Route component={App}>
{/* ... other routes */
</Route>
var App = React.createClass({
getInitialState () {
return { showBackButton: false }
},
componentWillReceiveProps(nextProps) {
var routeChanged = nextProps.location !== this.props.location
this.setState({ showBackButton: routeChanged })
}
})
Reference here: https://github.com/ReactTraining/react-router/issues/1066#issuecomment-139233328

React Router v4 - Redirect to home on page reload inside application

I need to redirect to home page when user refreshes other pages inside my application. I am using React router v4 and redux. Since the store is lost on reload, the page user reloaded is now empty and hence I want to take him back to a page that does not need any previous stored data. I don't want to retain state in localStorage.
I tried to handle this in event onload but it did not work:
window.onload = function() {
window.location.path = '/aaaa/' + getCurrentConfig();
};
You can try creating a new route component, say RefreshRoute and check for any state data you need. If the data is available then render the component else redirect to home route.
import React from "react";
import { connect } from "react-redux";
import { Route, Redirect } from "react-router-dom";
const RefreshRoute = ({ component: Component, isDataAvailable, ...rest }) => (
<Route
{...rest}
render={props =>
isDataAvailable ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/home"
}}
/>
)
}
/>
);
const mapStateToProps = state => ({
isDataAvailable: state.reducer.isDataAvailable
});
export default connect(mapStateToProps)(RefreshRoute);
Now use this RefreshRoute in your BrowserRouter as like normal Route.
<BrowserRouter>
<Switch>
<Route exact path="/home" component={Home} />
<RefreshRoute exact path="dashboard" component={Dashboard} />
<RefreshRoute exact path="/profile" component={ProfileComponent} />
</Switch>
</BrowserRouter>
It is so amazing that you don't want to keep state of user route map in browser but you use react-router!, the main solution for your case is do not use react-router.
If you don't use it, after each refresh the app come back to main view of app, If you wanna see route map in address bar without any reaction use JavaScript history pushState.
Hope it helps you.

Resources