I'm trying to handle a route like this
<Route path="/:first/:second?/:third?" />
where the amount of optional params after "first" are unknown. What's the best practice for approaching this in React Router v4?
Maybe you can do something fancy with path-to-regexp which is what react router uses to resolve paths, but making a custom route that parses the location and reformats it to dynamic paths seems pretty easy!
let NoArityRoute = ({ component }) => (
<Route
children={({ location }) => {
let params = location.pathname.split("/");
let first = params[1];
let rest = params.slice(2);
let path = `/:${first}${rest.map(x => `/:${x}?`).join("")}`;
return <Route path={path} component={component} />;
}}
/>
);
Then wherever else:
<NoArityRoute
component={({ match }) => (
<div>params: {JSON.stringify(match.params)}</div>
)}
/>
where match.params will be any number of dynamic args based on the url.
Related
I'm building the app with hierarchical structure and ability to set any slug url the user wants by the admin panel. For example, if the top page has url news and the page inside has url new_telescope, the whole url would be /news/new_telescope/. I wrote the url controller to handle this, so it really doesn't matter how many levels are there in the url - I get the data of the needed page from the server. So my React Router wouldn't have predefined urls - I need to take the current url, send it to server, get the data, get the component and render it. Now it looks like this:
function getRoute() {
url.splice(0, 1);
adminRequest(
'get',
constants.SERVER_ADDRESS + `/system/url/?url=` + JSON.stringify(url) + '&version=1',
).then(response => {
const CurrentComponent = lazy(() => import('./' + response.data.template));
return <Route path={window.location.pathname} element={<CurrentComponent page={response.data}/> } />
});
}
return (
<>
<Suspense fallback={<div className='preloader'><CircularProgress size='75px' /></div>}>
<Routes>
// trying to get the current url with proper component
{ getRoute() }
<Route path="/admin/" element={<Admin />} />
<Route path="/admin/login/" element={<Login />} />
...
</Routes>
</Suspense>
</>
);
So the problem is with the getRoute function. As you can see I make a request, get the component name from the DB and use lazy to import it. In that function I make a new Route to put it to Routes. But I get errors No routes matched location with any url. And of course the component doesn't render. I thought that Suspense will wait for the first route to appear but it doesn't work that way. How can I make a Router to wait for a dynamic route?
Issue
The getRoute function doesn't return anything. Sure, the Promise chain started from adminRequest returns some JSX, but that resolved value isn't returned by the other function.
React render functions are 100% synchronous functions. You can't call an asynchronous function and expect React to wait for any asynchronous code to resolve to render any content.
Solution
Use a useEffect hook to issue the side-effect of fetching the route. Unconditionally render the Route but conditionally render the element value.
const [data, setData] = useState();
const [CurrentComponent, setCurrentComponent] = useState();
useEffect(() => {
const getRoute = async () => {
url.splice(0, 1);
const response = await adminRequest(
"get",
constants.SERVER_ADDRESS +
`/system/url/?url=` +
JSON.stringify(url) +
"&version=1"
);
const CurrentComponent = lazy(() =>
import("./" + response.data.template)
);
setData(response.data);
setCurrentComponent(CurrentComponent);
};
getRoute();
}, [url]);
...
<Suspense fallback={<div className="preloader">Loading...</div>}>
<Routes>
<Route
path={/* whatever the path is */}
element={CurrentComponent ? <CurrentComponent page={data} /> : null}
/>
<Route path="/admin/" element={<Admin />} />
<Route path="/admin/login/" element={<Login />} />
</Routes>
</Suspense>
Demo
With the new React Router V6, the handling of routes changed a lot. I'm currently struggling to build the following structure
<BrowserRouter>
<Routes>
<Route path=":locale" element={<OtherRoutes/>} />
<Route path="*" element={<p>404</p>} />
</Routes>
</BrowserRouter>
The OtherRoutes component should return different paths based on the parent param, but this code does not work. I also tried with absolute paths, but this does not work as well.
const OtherRoutes = () => {
const {locale} = useParams();
if (locale === "de") {
return <Route path="seite" element={<GermanPage/>}/>
}
return <Route path="page" element={<EnglishPage/>}/>
}
Does anybody have an idea how to get this working?
Thanks
The solution was pretty simple. I just needed to make the first route :locale to match all routes like :locale/*
Let's say I have 2 components, app and project
I want to have control of elements in my project component from my app component.
When not using a route I can just do the following in my app component
const App = () => {
const ProjectRef = useRef(null);
return (
<Project ref={projectRef}>
);
}
Then in my project component I can get the reference as such
const Project = ({}, ref) => {}
My issue is how to achieve this when using a route. I have tried the following in my app.js component:
<Switch>
<Route path="/projects/:slug">
<Project ref={projectRef}/>
</Route>
</Switch>
This works in that I can get the ref, however in this method I can't seem to get access to the information about the route, which I need for this dynamic route.
Previously I did the following to get the slug
const Project = ({ match, location }) => {
// Get slug
const {params: { slug }} = match;
}
I tried the following, but it does not work, match is empty.
const Project = ({ match, location }, ref) => {}
I am looking for a way to achieve this ideally with functional components.
You are not passing the router props to the defined router component Project. So, just passing props to the component should do the work in your case.
Just replace this
<Switch>
<Route path="/projects/:slug">
<Project ref={projectRef}/>
</Route>
</Switch>
with this
<Switch>
<Route
path="/projects/:slug"
render={(props) => <Project {...props} ref={projectRef} />}
/>
</Switch>
In Gatsby, how would I restrict routes programmatically? Using react-router, I see it's possible to do a <Redirect> with <Route>, but how would this be implemented in Gatsby? To do something like this...
<Route exact path="/" render={() => (
loggedIn ? (
<Redirect to="/dashboard"/>
) : (
<PublicHomePage/>
)
)}/>
Where would I put this file in Gatsby? Would I put it in src/pages or elsewhere?
Edited, asking for additional clarification...
I'm able to get this work per the advice from #Nenu and the Gatsby docs. The docs gave a non-asynchronous example, so I had to tweak it for interacting with a remote server like this...
async handleSubmit(event) {
event.preventDefault()
await handleLogin(this.state)
.then(response => _this.setState({isLoggedIn: isLoggedIn()}))
.catch(err => { console.log(err) });
}
Also, I am able to use the <PrivateRoute /> with this.
Unfortunately though, when I render using...
render() {
if (isLoggedIn()) {
return <Redirect to={{ pathname: `/app/profile` }} />
}
return (
<View title="Log In">
<Form
handleUpdate={e => this.handleUpdate(e)}
handleSubmit={e => this.handleSubmit(e)}
/>
</View>
)
}
...while I do indeed <Redirect to={{ pathname:/app/profile}} />, I notice that a split-second before I redirect, the form fields are emptied and only after that do I get redirected to /app/profile (from /app/login). Also, if I type in an incorrect password, my whole form is re-rendered (re-rendering <View /> again). This would be a bad user-experience because they they'd have to re-enter all of their info from scratch, plus I'd be unable to add styling for invalid inputs, etc. I'm wondering if there is a better way to do this with Gatsby.
Or, would I have to build form functionality more from scratch (i.e., using Redux, Router, etc more directly) rather than depending upon Gatsby's higher level of abstraction?
Gatsby uses react-router under the hood, therefore you can define your client-only routes with it.
There is, as always with gatsby, a very nice example in the github repo:
https://github.com/gatsbyjs/gatsby/tree/master/examples/simple-auth
And the doc about it:
https://www.gatsbyjs.org/docs/building-apps-with-gatsby/#client-only-routes--user-authentication
To sum up, this is what is done:
1) Create a PrivateRoute component in /src/components
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
!isLoggedIn() ? (
// If we’re not logged in, redirect to the login page.
<Redirect to={{ pathname: `/app/login` }} />
) : (
<Component {...props} />
)
}
/>
);
2) Define routes in your wanted prefix "client-only" path
Let suppose you want to restrict access of the /app/:path section of your website, then in /src/pages/app.js:
const App = () => (
<div>
<PrivateRoute path="/app/profile" component={Home} />
<PrivateRoute path="/app/details" component={Details} />
<Route path="/app/login" component={Login} />
</div>
);
These routes will exist on the client only and will not correspond to index.html files in an app’s built assets. If you wish people to visit client routes directly, you’ll need to setup your server to handle these correctly. (source)
3) White-list the client-routes in gatsby-node.js
exports.onCreatePage = async ({ page, boundActionCreators }) => {
const { createPage } = boundActionCreators
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = `/app/:path`
// Update the page.
createPage(page)
}
}
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().