Currently if you do
createPage({path: "/item/:itemId", matchPath: "/item/:itemId", component: itemCompPath})
And when you go to /item/123
It's not straightforward at all how to extract the 123 value from the url elegantly.
I think Gatsby should make this usecase a lot better by providing those params as page props.
There are some mentionned examples here and there which rely on importing reach router <Router>
comp (which by the way does not play well at all with Typescript). This is probably the way to go if you have a lot of pages under the same match param like /authenticated/*
, but it's not convenient at all when you need to declare a single path.
There is also the Match
component that can be used (it plays better with TS)
Internally, Gatsby is using const { match: reachMatch } = require(
@reach/router/lib/utils)
to do that.
I think Gatsby should extract reach router params automatically for us and inject them directly under the page component. These params are already provided by match()
so Gatsby should be able to add these params to the props.
If you use the layout plugin, it is possible to extract the params using that plugin and inject them yourself in the page.
It took me some time to find the best/reusable solution for this problem, hope this will be helpful to others who need the same feature
import React from 'react';
// See https://github.com/reach/router/issues/74#issuecomment-397085896
// @ts-ignore
import { match } from '@reach/router/lib/utils';
export const extractPathParams = (props: any): any | null => {
if (props.pageContext.matchPath) {
const result = match(props.pageContext.matchPath, props.location.pathname);
if (result && result.params) {
return result.params;
}
}
return null;
};
const RootLayout = (props: any) => {
return React.cloneElement(props.children, extractPathParams(props));
};
export default RootLayout;
{
resolve: `gatsby-plugin-layout`,
options: {
component: require.resolve(`./src/layout/rootLayout.tsx`),
},
},
cc @DSchau
You can do this also
const getParamFromPathname = pathname =>
pathname
.split('/')
.pop()
// also works for page component, which has location props injected
export default Layout ({ location: { pathname } }) {
const param = getParamFromPathname(pathname)
// ...
}
One caveat is this works if pathname doesn't have trailing slash. To work with trailing slash e.g. /product/123/
const getParamFromPathname = pathname =>
pathname.slice(0, -1)
.split('/')
.pop()
My approach is matching routes with RegExp since Gatsby is built behind React Router
https://github.com/rayriffy/rayriffy-h/blob/master/gatsby-node.js#L270-L290
and then catch given parameters
https://github.com/rayriffy/rayriffy-h/blob/master/src/pages/g/index.js#L40
Yes parsing manually is always an option. It just feels a bit weird as you already define named params in the matchPath
to just not being able to access them in the page.
Parsing manually means you need to add a custom parsing logic to any page. The param is not always the 2nd arg. It could be the 3rd. There would be 3 params etc...
Doing pageContext.params.productId
looks way simpler to me
I don't recommend my solution, but in permits to encapsulate the pollution into a single place . It's a workaround I use. What I want is to not have to use a plugin or cloneElement and have it solved in Gatsby core.
Not stale! Chiming in here to say that I'd also love support for this. Parsing manually is OK for now, but if it were "official" then I'd feel better about handing work off to clients.
Also, Typescript complains when I add a matchPath
property on the createPage
input object. (matchPath doesn't exist on PageInput)
Match looks like a good idea, but I don't want to add this for every page, +1.
Hey, looks like we did a bad job on explaining client only routes 馃槃
To create a dynamic path you'll have to run
createPage({path: "/item/", matchPath: "/item/:itemId", component: itemCompPath})
In your itemCompPath you'll have to do something like
import { Router} from '@reach/router'
const App = () => (
<div className="app">
<Router>
<ItemComp path="item/:itemId" />
</Router>
</div>
);
const ItemComp = ({ itemId }) => {
return <p>Viewing: {itemId}</p>;
}
export default App;
A good example can be found at https://github.com/gatsbyjs/gatsby/blob/master/examples/client-only-paths/src/pages/index.js
Thank you for flying Gatsby! I hope this works for you and is a better workaround than Match
Hey @wardpeet that probably works but I don't like this solution at all.
The way Reach router works is not at all TS friendly. There's no property "path" on ItemComp.
If I have time I'll make a plugin that will do all that, and expose a "usePathParams
I opened a PR. Feedback is welcome 馃檹.
And fwiw, future version of react-router will expose a useParams
hook for exactly this purpose.
The way Reach router works is not at all TS friendly. There's no property "path" on ItemComp.
Oh that's a bummer, well you can override the reach router types 馃し鈥嶁檪. I know it's not ideal but from a Gatsby point of view we can't do much, or am I missing something?
sure @wardpeet you can do something, just expose matched params in props.pageContext.matchParams
like @universse have used in his PR
I do this in my appin a generic way, at the layout level (layout v1 with the plugin) and it's way more convenient than importing reach router and doing the code you mention above for each dynamic page. (not all pages are under a /client/* prefix, so we have to put this router code in multiple places)
Feel free to open up an issue to discuss this topic and best approaches before creating a PR. In the meantime I'll close this PR :)
@LekoArts do you have a best approach to suggest for implementing this feature?
For me the best approach looks like what @pieh suggest here, and make sure Gatsby core forwards those matched params to pages:
https://github.com/gatsbyjs/gatsby/pull/17384#discussion_r321011040
The route param is already included as @reach/router pass the param to children using React.cloneElement. You can access it in pageProps.
So it's a matter of documentation rather than code changes.
Doing my first Gatsby project, came across this post and wondered if what I'm doing now is similar:
gatsby-config.js
{
resolve: `gatsby-plugin-create-client-paths`,
options: { prefixes: [`/card/*`] },
},
Then I can just use this.props["*"]
in pages/card.js
Is this unique to that plugin?
The concept behind it is client-only routes. The plugin serves to simplify the process a little bit.
Hey again!
It鈥檚 been 30 days since anything happened on this issue, so our friendly neighborhood robot (that鈥檚 me!) is going to close it.
Please keep in mind that I鈥檓 only a robot, so if I鈥檝e closed this issue in error, I鈥檓 HUMAN_EMOTION_SORRY
. Please feel free to reopen this issue or create a new one if you need anything else.
As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out gatsby.dev/contribute for more information about opening PRs, triaging issues, and contributing!
Thanks again for being part of the Gatsby community!
Apparently the current version of Gatsby does exactly what you asked for, @slorber.
I created this createPage entry:
createPage({
path: `/booking/:bookingId`,
matchPath: `/booking/:bookingId`,
component: bookingTemplate,
})
And the following component:
import React from "react"
export default function Booking({ bookingId }) {
return <div>Booking #{bookingId}</div>
}
And bookingId
comes through as you mentioned 馃憤
Hi
I actually found out it is indeed working as I expected yes. Sorry I forgot to report it here.
@brisa-pedronetto's example generates one static page in gatsby-node
, then the React component renders it.
Is it possible to generate multiple static pages for existing bookingId
s, and render newly created ones dynamically, with the same template? The use case is items that get created rarely, but accessed frequently, so SSR would help. However, when an admin creates an item, it should be rendered right away, rather than waiting for Gatsby to rebuild.
I have the equivalent of,
for (const booking of bookings)
actions.createPage({
path: `/bookings/${booking.id}`,
component: resolve('./src/templates/booking.tsx'),
context: {
booking,
},
// matchPath: '/bookings/:id',
});
which generates pages for the existing bookings via booking.tsx (which takes the booking via pageContext
). But if I uncomment the matchPath
, then Gatsby only generates one static page, the first one. If I change the path
line to path: '/bookings/:id'
, it generates only one static page, for the last booking in the array.
@dandv if you want hybrid static/dynamic you'd rather use both at the same time, and make 2 separate page templates (you can share as much code as you want between the 2 templates)
bookings.forEach(booking => {
actions.createPage({
path: `/bookings/${booking.id}`,
component: resolve('./src/templates/staticBooking.tsx'),
context: {
booking,
},
});
});
createPage({
path: `/booking/:bookingId`,
matchPath: `/booking/:bookingId`,
component: resolve('./src/templates/dynamicBooking.tsx'),
})
That worked, thanks. For the dynamic route, I noticed that it doesn't matter when you pass in the path
after /bookings
(:WHATEVER
worked), so I left it at path: '/bookings'
.
@slorber: we're using that solution, and the dynamic route does show the item after creation in the same browser, but opening it in an incognito window returns a "Not found". Does Gatsby cache it in the Service Worker or something? (Demo - go through the create flow; for the text area, just select all the text on the page and paste it in there to pass the min length validation)
How can we force dynamic rendering like with a regular React app, for Not Found routes?
yes @dandv I think path is not so useful if you provide a matchPath. Worth double checking because I think I remember it might affect the path of the html page written in the build folder.
Sorry @civility-bot I don't know what you are asking me. Make a small repro in a public sandbox would be easier to inspect.
in your example there a 2 different paths (a little similar but not exact)
/bookings/:id
/booking/:id
but is it possible to have there 2 exact same paths?
/bookings/:id
<- static
/bookings/:id
<- dynamic
so for the user there is no difference between a dynamic or a static detail page?
it should be possible yes
bookings.forEach(booking => {
actions.createPage({
path: `/bookings/${booking.id}`,
component: resolve('./src/templates/staticBooking.tsx'),
context: {
booking,
},
});
});
createPage({
matchPath: `/booking/:bookingId`,
component: resolve('./src/templates/dynamicBooking.tsx'),
})
In the 2nd exemple afaik the path does not matter much.
I think it affects the static file written to disk, but you'd rather try and see yourself.
that means i need on the second createPage a different path to have different files in the folder?
i did not tested this ... but thx for the hints
Most helpful comment
Hey, looks like we did a bad job on explaining client only routes 馃槃
To create a dynamic path you'll have to run
In your itemCompPath you'll have to do something like
A good example can be found at https://github.com/gatsbyjs/gatsby/blob/master/examples/client-only-paths/src/pages/index.js
Thank you for flying Gatsby! I hope this works for you and is a better workaround than Match