Adding Redirects programmatically to React Router - javascript

I'm trying to add a bunch of redirects given an array of old routes and new routes so I came out with this component:
const redirectUrls = [
{ oldUrl: '/robin', newUrl: '/users' },
{ oldUrl: '/batman', newUrl: '/courses' }
];
export default Redirects = () => (
redirectUrls.map((url, index) => <Route key={index} exact path={url.oldUrl} render={() => <Redirect to={url.newUrl} />} />)
);
Which works fine, but I wanted it to be simplier so I removed the Route and left only the redirect like this:
export default Redirects = () => (
redirectUrls.map((url, index) => <Redirect key={index} exact from={url.oldUrl} to={url.newUrl} />))
But it won't work, all the redirects take me to the last route, which in this case is /courses, and its weird because if I do this:
<Switch>
...
<Route path='/users' exact component={users} />
<Route path="/courses" exact component={CoursesList} />
<Redirect from='/robin' to='/users' />
<Redirect from='/batman' to='/courses' /> // having them like this works fine
</Switch>
So it makes no sense for them fail when I create them with a map and I haven't found anything that leads to the cause nor to a solution.

You need to wrap the exported redirects in a switch:
const Redirects = () => {
return (
<Switch>
{redirectUrls.map(url => (
<Redirect from={url.oldUrl} to={url.newUrl} />
))}
</Switch>
);
};
export default Redirects;
That's only if you actually need to make a component out of them. It's not totally clear what you need and are looking for since export default Redirects = () => is invalid syntax. If you just want to bundle them as some variable you can do:
export const Redirects = redirectUrls.map(url => <Redirect from={url.oldUrl} to {url.newUrl} />);
// App.js
<Switch>
<Route path='/users' exact component={users} />
<Route path="/courses" exact component={CoursesList} />
...
{Redirects}
</Switch>

Did you try this with exact props
<Switch>
...
<Route path='/users' exact component={users} />
<Route path="/courses" exact component={CoursesList} />
<Redirect exact from='/robin' to='/users' />
<Redirect exact from='/batman' to='/courses' /> // having them like this works fine
</Switch>
Reference https://github.com/ReactTraining/react-router/issues/4837

Related

react-router render is not working; routing is not working

I'm making wizard form in react
and have got a problem with routing
page routing is like this
home -> wizard form(step1) -> step2 ->step3..
<Route
path="/startAProgram/step1"
exact
render={() => <StartAProgram />}
/>
//app.js
//There are many routes in app.js and one of routes is StartAProgram(wizard form)
function StartAProgram() {
return (
<WrapperDiv>
<Stepper />
<ProgramName /> {/* this is step 1 */}
<Router>
{/* <Route path="startAProgram/step1" render={() => <ProgramName />} /> */}
<Switch>
<Route
path="startAProgram/step2"
exact
render={() => <SetTargets />}
/>
<Route
path="startAProgram/step3"
render={() => <ParticipationGuidelines />}
/>
</Switch>
</Router>
</WrapperDiv>
);
}
react-router-dom version:^5.2.0
function ProgramName() {
const programName = useSelector((state) => state.programName);
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
defaultValues: programName,
});
const dispatch = useDispatch();
const history = useHistory();
const handleOnSubmit = (data) => {
sessionStorage.setItem('programName', Object.values(data));
dispatch(submitStep1(data));
history.push('./step2');
console.log('next~');
};
return (
<form onSubmit={handleSubmit(handleOnSubmit)}>
<Text size={30} weight={800} mb={10}>
Step 1. Program name
</Text>
<label htmlFor="programName">
Program Name
<input
name="programName"
{...register('programName', { required: 'fill out!' })}
/>
<ErrorMessage errors={errors} name="programName" as="p" />
</label>
<Button type="submit">next</Button>
</form>
);
}
It would be Step2 if the form validation is successful but Step2 component rendering is not working currently.
Page Step 1 is rendering well, but it is not letting me render step 2 and more.
Anyone can solve this problem?
Issue
StartAProgram is rendering these other routes, so when you navigate to one of those routes, the route rendering StartAProgram is no longer matched and those other routes you are linking to are no longer mounted.
Solution
Make the root stepper route path more generic so it can match rendering any of the steps.
<Route path="/startAProgram" component={StartAProgram} />
Then refactor StartAProgram to render ProgramName back on a ".../step1" path. Remove the extra Router component, you only need one router per app.
function StartAProgram() {
return (
<WrapperDiv>
<Stepper />
<Switch>
<Route path="/startAProgram/step1" component={ProgramName} />
<Route path="/startAProgram/step2" component={SetTargets} />
<Route
path="/startAProgram/step3"
component={ParticipationGuidelines}
/>
</Switch>
</WrapperDiv>
);
}
When nesting descendent routes it is a common pattern to use the useRouteMatch hook to dynamically build relative paths.
import { Switch, Route, useRouteMatch } from 'react-router-dom';
function StartAProgram() {
const { path } = useRouteMatch();
return (
<WrapperDiv>
<Stepper />
<Switch>
<Route path={`${path}/step1`} component={ProgramName} />
<Route path={`${path}/step2`} component={SetTargets} />
<Route path={`${path}/step3`} component={ParticipationGuidelines} />
</Switch>
</WrapperDiv>
);
}
This is helpful/useful if you need to move the StartAProgram component around onto a different path, the links and routes from here work relative to the current location.

Bind slug like the first URL segment

I'm developing react app use react-router-dom. I have unusual the app behavior. The app doesn't have home page. We can go to the app use 'slug'. For example: http://siteExample.net/my-subsite. If my-subsite exist in our database We need to work with this slug.
My code snippet for routes:
const App = ({ isAuth, onSetSlug, slug}) => {
useEffect(() => {
onSetSlug(window.location.pathname.split('/')[1])
}, [])
return (
<Router>
<div className="App">
<Suspense fallback={<Loading />}>
<MainModal />
<ScrollToTop>
{!isAuth ? (
<Switch>
<Route path={'/:slug'} exact component={lazy(() => import('./containers/notAuth/home'))} />
<ContentWrapper
container
component="main"
adaptSidebar={5}
adaptContent={7}
>
<Switch>
<Route path={'/:slug/login'} exact component={lazy( () => import('./containers/notAuth/login'))} />
<Route path={'/:slug/signup'} exact component={lazy(() => import('./containers/notAuth/signup'))} />
<Redirect to={'/:slug'} />
</Switch>
</ContentWrapper>
<Redirect to={'/:slug'} />
</Switch>
</ScrollToTop>
</Suspense>
</div>
</Router>
)}
There is a issue that :slug/login doesn't replace on my-subsite/login for example.By the way it's working only for my local machine. When I publish it to remote host it doesn't work completely.
Remove the exact prop on the outer/root route since it necessarily excludes matching any subroutes.
<Switch>
<Route
path={'/:slug'} // <-- want this path prefix to match
component={lazy(() => import('./containers/notAuth/home'))}
/>
<ContentWrapper
container
component="main"
adaptSidebar={5}
adaptContent={7}
>
<Switch>
<Route
path={'/:slug/login'}
exact
component={lazy( () => import('./containers/notAuth/login'))}
/>
<Route
path={'/:slug/signup'}
exact
component={lazy(() => import('./containers/notAuth/signup'))}
/>
<Redirect to={'/:slug'} />
</Switch>
</ContentWrapper>
<Redirect to={'/:slug'} />
</Switch>

How to detect matched route from a component outside of the <Route/> component that was matched using react-router?

I've got the following structure in my React app, using react-router-dom.
<Router>
<Header/>
<Main>
<AllRoutes> // THIS HANDLES THE SWITCH WITH ALL THE ROUTES
<Switch>
<Route exact path={ROUTES.HOME} component={Home}/>
<Route exact path={ROUTES.ABOUT} component={About}/>
<Route exact path={ROUTES.PRIVACY} component={Privacy}/>
// ETC
</Switch>
</AllRoutes>
</Main>
<Footer/> // <==== FOOTER NEEDS TO KNOW WHICH ROUTE HAS BEEN MATCH
<Router>
QUESTION
Footer needs to know what <Route/> has been match. What is the best pattern to achieve that?
OPTION #1
I found the useRouteMatch hook over on react router docs:
This would kind of work, but I don't think it is good enough for my situation. Because a URL string can match a route and still don't be a valid route at the same time.
For example:
Route: /:language/privacy
Valid route: /en/privacy
Not valid route that would also match: /notALanguage/privacy
Once a route has match, I usually need to check if it is valid before rendering a component page or the 404 page.
Like:
<Route exact path={"/:language/privacy"} render={(routeProps) => {
const possibleLanguage = routeProps.match.params.language;
if (possibleLanguage in LANGUAGES) {
return(
<PrivacyPage lang={possibleLanguage}/>
);
}
else {
return(
<Page404/>
);
}
}}/>
OPTION #2
What I'm thinking about doing is:
App.js calls useLocation. So it always re-render when there is a route change.
I could add a detectRoute function in App.js to do all the route checking beforehand.
And my AllRoutes component wouldn't need a component. I would implement a native JS switch and render the corresponding route.
This way I know upfront which <Route/> is going to match and I can pass it on to <Footer/> or any component that lives outside of the matched <Route/>.
Something like this:
SandBox Link
export default function App() {
console.log("Rendering App...");
const location = useLocation();
// THIS WOULD BE THE detectRoute FUNCTION
// I COULD EVEN USE THE useRouteMatch HOOK IN HERE
const matchedRoute =
location.pathname === ROUTE1
? "ROUTE1"
: location.pathname === ROUTE2
? "ROUTE2"
: "404";
return (
<div>
<div className="App">
<Link to={ROUTE1}>Route 1</Link>
<Link to={ROUTE2}>Route 2</Link>
<Link to={"/whatever"}>Route 404</Link>
</div>
<div>
<AllRoutes matchedRoute={matchedRoute} />
</div>
</div>
);
}
function AllRoutes(props) {
switch (props.matchedRoute) {
case "ROUTE1":
return <Route exact path={ROUTE1} component={Page1} />;
case "ROUTE2":
return <Route exact path={ROUTE2} component={Page2} />;
default:
return <Route exact path={"*"} component={Page404} />;
}
}
It works. But I would like to know if there's a proper way of doing this, 'cause this seems a bit weird and there might be something out there that was specifically designed for this.
Generally you want to either:
Wrap the components together
Create another switch to route them (and pass match params)
I put together a somewhat comprehensive example of the options. Hope that helps!
import React from "react";
import "./styles.css";
import { Switch, Link, Route, BrowserRouter } from "react-router-dom";
const hoc = (Component, value) => () => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const Wrapper = ({ component: Component, value }) => (
<>
<main>
<Component />
</main>
<Footer value={value} />
</>
);
const WrapperRoute = ({ component, value, ...other }) => (
<Route
{...other}
render={props => <Wrapper component={component} value={value} {...props} />}
/>
);
const Footer = ({ value }) => <footer>Footer! {value}</footer>;
const Header = () => <header>Header!</header>;
const Another = () => <Link to="/onemore">One More!</Link>;
const Home = () => <Link to="/other">Other!</Link>;
const OneMore = () => <Link to="/">Home!</Link>;
const Other = () => <Link to="/another">Another!</Link>;
export default () => (
<BrowserRouter>
<Header />
<Switch>
{/* You could inline it! */}
<Route
path="/another"
render={() => (
<>
<main>
<Another />
</main>
<Footer value="" />
</>
)}
/>
{/* You could use a custom route component (that uses an HOC or a wrapper) */}
<WrapperRoute
component={OneMore}
path="/onemore"
value="I got one more!"
/>
{/* You could use a Higher-Order Component! */}
<Route path="/other" component={hoc(Other, "I got other!")} />
{/* You could use a wrapper component! */}
<Route
path="/"
render={() => <Wrapper component={Home} value="I got home!" />}
/>
</Switch>
{/* You could have another switch for your footer (inline or within the component) */}
<Switch>
<Route
path="/another"
render={() => <Footer value="Switch footer another!" />}
/>
<Route
path="/other"
render={() => <Footer value="Switch footer other!" />}
/>
<Route
path="/onemore"
render={() => <Footer value="Switch footer onemore!" />}
/>
<Route path="/" render={() => <Footer value="Switch footer home!" />} />
</Switch>
</BrowserRouter>
);
Note the WrapperRoute would allow you to do validation on your match params before passing them through. You could do a Redirect if needed.
What I've ended up doing:
Since I'm using Redux, I added a piece of global state to keep track of the matched route.
And I dispatch actions to update that state from the render prop from the <Route/>'s component.
<Switch>
<Route key={index} exact path={"/some-route"} render={(routeProps) => {
// HERE I DISPATCH AN ACTION TO CHANGE THE STATE FOR THE CURRENT ROUTE
dispatch({
type: UPDATE_CURRENT_ROUTE,
payload: { name: "SOME_ROUTE_NAME" }
});
return (
<PrivacyPage
{...routeProps}
/>
);
}}/>
</Switch>
And now I can do on Footer.js:
function Footer() {
const currentRoute = useSelector((state) => state.currentRoute);
// RENDER FOOTER ACCORDINGLY TO THE CURRENT ROUTE
}

React Router path var with other paths as well, React cant tell the difference

I am setting up react router for different link in my project but the problem is I need react router to tell the difference between a user username variable and other paths.
For example:
baseUrl/admin
baseUrl/daniel
React doesnt know the difference. I will have a list of usernames in a db and would return an error if the user doesnt exist then that means the page does not exist.
This is my code:
class App extends Component{
render(){
return (
<Router>
<Route exact path="/" render={props => (
<React.Fragment>
<h1>Hey</h1>
</React.Fragment>
)}
/>
<Route exact path="/admin" render={props => (
<React.Fragment>
<h1>admin</h1>
</React.Fragment>
)}
/>
<Route path="/:user" component={UserComponent}
/>
</Router>
);
}
}
You can use the match.url property to choose which component render, for example:
<Route path="/:user" render={props => {
if(props.match.url === '/admin') {
return (
<React.Fragment>
<h1>Hey</h1>
</React.Fragment>
)
} else return (<UserComponent {...props} />)
}} />

Organizing React routes into separate components

I'm trying to find a way to organize my routes to assist the dev who might be taking over my work in the future. I thought of separating my <Route /> entries into separate components and then just load those into a main component similar to how users are assigned groups.
The issue is that when using more than one component only the first one works. This might not be the most react way of doing this so I'm also open to alternatives.
Original route arrangement
const AllRoutes = () => {
return (
<Switch>
{/* public routes*/}
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
{/* auth routes */}
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Route component={Error404View} />
</Switch>
)
}
Separating the public routes from the auth ones:
const PublicRouteGroup = () => {
return (
<>
<Route path={'/about'} component={AboutView} />
<Route path={'/project'} component={ProjectView} />
<Route path={'/contact'} component={ContactView} />
</>
)
}
const AuthRouteGroup = () => {
return (
<>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
</>
)
}
This way I can use it as such:
const AllRoutes = () => {
return (
<Switch>
<PublicRouteGroup /> {/* This works */}
<AuthRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Flipping <PublicRouteGroup /> and <AuthRouteGroup /> only changes the order:
const AllRoutes = () => {
return (
<Switch>
<AuthRouteGroup /> {/* This works */}
<PublicRouteGroup /> {/* This doesn't */}
{/* This 404 is not a route group */}
<Route component={Error404View} />
</Switch>
)
}
Update #1
This is thanks to #skyboyer. By moving the <Switch> to the child components and removing it from the AllRoutes component each component started to show. It appears adding the <Switch> in AllRoutes is allowing only the first hit to show which is as <Switch> does. But now by removing it it shows the 404 at the end of each page as well.
Basically, it looks like this:
const AllRoutes = () => {
return (
<>
<Route component={AuthRouteGroup} /> {/* This works */}
<Route component={PublicRouteGroup} /> {/* This also works */}
{/* This 404 is not a route group */}
<Route component={Error404View} /> {/* Always shown at the bottom */}
{/* Even putting the 404 in its own RouteGroup yields the same issue */}
</>
)
}
It appears this current set up of treating components like OOP classes you can extend from is the wrong approach. I've instead made use of arrays since these can be acted upon by the spread operator. It still accomplishes the same goal of organizing routes across an infinite number of groups which was what I was after.
Create the array for each group
const public_route_group = [
{path: '/about', component: AboutView},
{path: '/project', component: ProjectView},
{path: '/contact', component: ContactView},
]
const auth_route_group = [
{path: '/login', component: LoginView},
{path: '/logout', component: LogoutView},
]
const error_route_group = [
{component: Error404View} // No path required
]
const user_routes = [
...public_route_group,
...auth_route_group,
...error_route_group
]
Create the routes
const AllRoutes = () => {
return (
<Switch>
{user_routes.map((route, idx) => {
return <Route key={idx} {...route} />
})}
</Switch>
)
}
I figure this can also be modified further if you're using nested objects in your array.
I'd like to thank #skyboyer for providing an insight into this problem.
How about having it without Swtich at top-level
<Route component={PublicRouteGroup} />
<Route component={AuthRouteGroup} />
so they are rendered unconditionally. And then having extra Switch in your components like
const AuthRouteGroup = () => {
return (
<Switch>
<Route path={'/login'} component={LoginView} />
<Route path={'/logout'} component={LogoutView} />
<Switch/>
)
}
But why id did not work?
The reason is how Switch works:
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
See, even if AuthRouteGroup is not a Route, Switch anyway looks to its props.path. And once undefined for props.path matches any path and Switch renders only first matching Route you are getting only first component rendered.
[UPD] "does-not-match-any-route" View will work only at top level of Switch. Also there are no way to know if some nested children of sibling element has matched current route or not. So only way I see is listing all routes in single place.
Alternative that looks rather poor is having special route "/error404" and redirect user to it from inside of other components(but who should decide? and where? and when?).

Categories

Resources