Gatsby: Gatsby clientside pages: inject route match params as props

Created on 10 May 2019  路  26Comments  路  Source: gatsbyjs/gatsby

Summary

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.

Details

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.

Workaround

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

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

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

All 26 comments

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()" hook. Already have this on my app. It's much more generic and does not require to add a comp which is quite unintuitive imho

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 bookingIds, 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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rossPatton picture rossPatton  路  3Comments

Oppenheimer1 picture Oppenheimer1  路  3Comments

benstr picture benstr  路  3Comments

theduke picture theduke  路  3Comments

signalwerk picture signalwerk  路  3Comments