How to redirect with match params in React Router v6? - react-router

I used to do this in react router v5:
<Redirect exact from="/:org" to="/:org/users" />
translating it into this doesn't work:
<Route path="/:org" element={<Navigate replace to="/:org/users" />} />
What is the correct way to perform this kind of a redirect?
UPD: To clarify – I don't have a separate route for /:org/users at the same routes level but I have /:org/* route that handles /:org/users and others:
<Route path="/:org/*" element={<OrgPagesComponent />} />

so far I've come up with the following solution:
const OrgRedirect = () => {
const { org } = useParams();
return <Navigate to={`/${org}/users`} />
}
<Route path="/:org" element={<OrgRedirect />} />

Use relative navigation for the redirect.
<Route path="/:org/*" element={<OrgPagesComponent />} />
<Route path="/:org" element={<Navigate to="users" replace />} />
When the path matches exactly "/:org" the second Route will be matched and render the Navigate which will relatively navigate to "/:org/users" which is matched by the first Route on path "/:org/*".
Here is a running codesandbox demo.

I am using this helper Component to pass params to Navigate:
function NavigateWithParams({ to, ...rest }) {
const params = useParams();
let toValue;
if (typeof to === 'function') {
toValue = to(params);
} else {
toValue = to;
}
return <Navigate to={toValue} {...rest} />
}
Usage:
<Route
path="/test/:id/results"
element={<NavigateWithParams
to={params => `/test/${params.id}/report`}
replace
/>}
/>

Related

Nesting React routes to login-protected pages [duplicate]

This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed 8 months ago.
I am using react-router-dom#6.3.0
I have created a React app where certain Private pages are accessible only users who have logged in.
You can find a demo here, and a GitHub repo here.
A simplified version of this is shown below.
I have wrapped every Private page in its own RequireLogin component, and this works:
<Route
path="/private1"
element={
<RequireLogin redirectTo="/">
<Private
text="Private Page #1"
/>
</RequireLogin >
}
/>
The RequireLogin component redirects to a page with the Login component if the user is not logged in, and renders the requested component only to a logged in user.
My question is this:
Is it there a way to wrap all the Private routes inside one RequireLogin component, or do I have to wrap each one separately?
import React, { createContext, useContext, useState } from 'react'
import ReactDOM from 'react-dom';
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
NavLink
} from "react-router-dom";
const UserContext = createContext()
const UserProvider = ({children}) => {
const [ loggedIn, logIn ] = useState("")
return (
<UserContext.Provider
value={{
loggedIn,
logIn
}}
>
{children}
</UserContext.Provider>
)
}
function App() {
return (
<Router>
<UserProvider>
<Routes>
<Route
path="/"
element={<NavLink to="/login">Log In</NavLink>}
/>
<Route
path="/login"
element={<Login />}
/>
<Route
path="/private1"
element={
<RequireLogin redirectTo="/login">
<Private
text="Private Page #1"
/>
</RequireLogin >
}
/>
<Route
path="/private2"
element={
<RequireLogin redirectTo="/login">
<Private
text="Private Page #2"
/>
</RequireLogin >
}
/>
</Routes>
</UserProvider>
</Router>
);
}
function Menu({hideLogOut}) {
const { loggedIn } = useContext(UserContext)
if (loggedIn) {
if (!hideLogOut) {
return <ul>
<li><NavLink to="/login">Log Out</NavLink></li>
<li><NavLink to="/private1">Private #1</NavLink></li>
<li><NavLink to="/private2">Private #2</NavLink></li>
</ul>
} else {
return <ul>
<li><NavLink to="/private1">Private #1</NavLink></li>
<li><NavLink to="/private2">Private #2</NavLink></li>
</ul>
}
} else {
return <p>Not Logged In</p>
}
}
function RequireLogin ({ children, redirectTo }) {
const { loggedIn } = useContext(UserContext);
return loggedIn
? children
: <Navigate to={redirectTo} />;
}
function Private({text}) {
return (
<div>
<Menu />
<h1>{text}</h1>
</div>
)
}
function Login() {
const { loggedIn, logIn } = useContext(UserContext)
const toggleLogged = () => {
logIn(!loggedIn)
}
return (<div>
<Menu
hideLogOut={true}
/>
<label htmlFor="loggedIn">
<input
type="checkbox"
name="loggedIn"
id="loggedIn"
checked={loggedIn}
onChange={toggleLogged}
/>
Pretend that we are logged in
</label>
</div>)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
I use a second router for the private routes, wrapped with a single <RequireLogin>. Example:
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegistrationPage />} />
<Route path="*" element={
<RequireLogin>
<Routes>
<Route path="/" element={<FeedPage />} />
<Route path="/explore" element={<ExplorePage />} />
<Route path="/user/:username" element={<UserPage />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</RequireLogin>
} />
</Routes>

react Browser URL changed but useLocation hook doesn't get update

I am creating a dashboard using nested routes. But when I tries clicking a Link, the URL on the browser does change but the useLocation hook doesn't get update so my useEffect hook doesn't get fired to reload data.
This is my App main routes:
function App() {
return (
<Router>
<Header />
<Switch>
<Route path="/login" component={ Login } />
<ProrectedRoute exact path="/" component={ Home } />
<ProrectedRoute path="/blogs/:title" component={ BlogPage } />
<ProrectedRoute path="/new" component={ NewBlog } />
<ProrectedRoute path="/dashboard/:part" component={ UserDashboard } />
<ProrectedRoute path="/:name" component={ ProfilePage } />
</Switch>
<Footer />
</Router>
);
}
And this is my dashboard:
export const UserDashboard = () => {
const dispatch = useDispatch();
const location = useLocation();
useEffect(() => {
switch(location) {
case '/dashboard/posts':
dispatch(getOwnPosts());
break;
case '/dashboard/bookmarks':
dispatch(getBookmarkPosts());
break;
case '/dashboard/followings':
break;
default:
break;
};
}, [location]);
return (
<div className="dashboard">
<Router>
<div className="dashboard__sidebar">
<Link to="/dashboard/posts">Your posts</Link>
<Link to="/dashboard/bookmarks">Your bookmarks</Link>
<Link to="/dashboard/followings">Your followings</Link>
</div>
<div className="dashboard__main">
<Switch>
<Route exact path="/dashboard/posts">
<BlogRecommendationList selector={selectUserPosts} />
</Route>
<Route exact path="/dashboard/bookmarks">
<BlogRecommendationList selector={selectUserBookmarkedPosts} />
</Route>
</Switch>
</div>
</Router>
</div>
)
}
I have found the error. It is because I use 2 Router in the same application (1 in the App.js and 1 in Dashboard.js). There should be only 1 router in an application. Remove Router from dashboard makes it work.

React router dom v5 Nested routes never matching

I have two main pages: LoginPage and HomePage. HomePage is main page with navbar and content. I'm trying to implement nested routing.
loginPage
HomePage
SchoolsPage
UserDetails
I prepared routing for them:
<Switch>
<ProtectedRoute
key="loginPage"
path={`${ROOT_PATHS.login}`}
component={AuthPageContainer}
shouldBeRedirected={authState === 'AUTHENTICATED'}
authenticationPath={`${ROOT_PATHS.version}`}
/>
<ProtectedRoute
key="homePage"
path={ROOT_PATHS.version}
component={HomePageContainer}
shouldBeRedirected={authState === 'NOT_AUTHENTICATED'}
authenticationPath={`${ROOT_PATHS.login}`}
/>
</Switch>
Home page:
export const HomePage: React.FC<HomePageProps> = ({ fetchUserInfo, user, isLoading, userLogout }): JSX.Element => {
useEffect(() => {
fetchUserInfo()
}, [fetchUserInfo])
return (
<LoadingContent isLoading={isLoading}>
<Navbars user={user} userLogout={userLogout} />
<Pages />
</LoadingContent>
)
}
An routings in Pages:
export const Pages: React.FC = (): JSX.Element => {
const { url, path } = useRouteMatch()
return (
<Switch>
<Route path={`${url}${ROOT_PATHS.userDetails}`} exact component={UserDetailsContainer} />
<Route path={`${url}${ROOT_PATHS.schools}`} exact component={SchoolListContainer} />
<Route path={`${path}`} exact>
{console.log('redirected')}
<Redirect to={`${url}${ROOT_PATHS.schools}`} />
</Route>
</Switch>
)
}
And paths:
export const ROOT_PATHS = {
login: '/login',
version: '/v1',
schools: '/schools',
userDetails: '/userDetails'
}
My index.tsx:
// axios should be configured before saga because saga makes requests on startup
configureAxios()
const store = configureStore()
export const DOMStructure: React.FC = (): JSX.Element => (
<Suspense fallback={<Spinner animation="border" role="status" />}>
<Provider store={store}>
<ConnectedRouter history={history}>
<CookiesProvider>
<AppContainer />
</CookiesProvider>
</ConnectedRouter>
</Provider>
</Suspense>
)
ReactDOM.render(<DOMStructure />, document.getElementById('root'))
It all works fine when navigating via links in navbar.
But when I refresh page, it never matches to any child-routes and is redirected to v1/schools (I checked that with console.log('redirected').
So I cannot render page under specific ur. When I put 'localhost:3000/v1/userDetails' in browser, it's always redirected to 'localhost:3000/v1/schools'.
Why is that? What i'm missing?

How to access the path before redirect (by replace())?

When visitors come to my website, if they're not logged in, they'll be redirected to a login page. This is done by the onEnter() (see code below). When redirect happens, I'd like to access and store the original path. But somehow I keep getting the post-redirect path, i.e. /login, from the redux store. How can I get the original one?
Here is the simplified code in my routes.js (I also use redux, hence the store).
...
export default (store) => {
const requireLogin = (nextState, replace, cb) => {
function checkAuth() {
const { auth: {loggedIn} } = store.getState();
if (!loggedIn) {
replace('/login');
}
cb();
}
console.log(store.getState().routing.location.path); // here I always get the post-redirect path as opposed to the pre-redirect path
if (!isAuthLoaded(store.getState())) {
store.dispatch(loadAuth()).then(checkAuth);
} else {
checkAuth();
}
};
return (
<Route path="/" component={App}>
<Route onEnter={requireLogin}>
<Route path="postLoginPage" component={postLoginPage} />
</Route>
<Route path="login" component={Login} />
</Route>
);
};

React Router, this.props.children == null when refreshing page

So I've got React-Router set up as follows:
// App.js
render(){
console.log(this.props)
return (
<div>
<Header />
{this.props.children}
<Footer />
</div>
)
}
// routes.js
export default (
<Router>
<Route path="/" component={App} >
<IndexRoute component={HomePage} />
<Route path="/destination" component={DestinationIndexHandler} />
<Route path="/destination/:slug" component={DestinationHandler} />
<Route path="/destination/:slug/:article" component={ArticleHandler} />
<Route path="/article" component={ArticleIndexHandler} />
<Route path="/article/:slug" component={ArticleHandler} />
<Route name='contact' path="/contact" component={ContactHandler} />
<Route path="*" component={NotFoundHandler}/>
</Route>
</Router>
);
And finally:
// server.js
app.get(/^(?!.*(api|.js|.css)).*$/, function (req, res) {
let location = createLocation(req.url);
console.log("LOCAL")
match({ routes, location }, (error, redirectLocation, renderProps) => {
console.log(location);
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
let content = ReactDOMServer.renderToString(<RoutingContext {...renderProps} />);
console.log(content);
res.status(200).render('index', {
content: content,
staticPort: DEV_PORT
});
} else {
res.status(404).send('Not found')
}
})
});
For some reason, when I refresh the page, the server side renders the view successfully but the browser renders the home view (IndexRoute). If you remove the IndexRoute, it only renders the header and footer.
If I log in the terminal I get 3 returns, the first and second being:
CHILDREN { '$$typeof': Symbol(react.element),
type: [Function: ContactHandler],
and finally a call to NotFoundHandler, even though the route is there. Any ideas?