diff --git a/content/posts/2022.05.01.react-navigation-path-as-parameter.md b/content/posts/2022.05.01.react-navigation-path-as-parameter.md new file mode 100644 index 0000000..4867580 --- /dev/null +++ b/content/posts/2022.05.01.react-navigation-path-as-parameter.md @@ -0,0 +1,135 @@ +--- +title: "Using a path as a parameter in React Navigation" +date: 2022-05-01T17:49:02-04:00 +draft: true +toc: false +images: +tags: + - dev + - react + - javascript + - typescript +--- + +I've been trying to integrate [React Navigation](https://reactnavigation.org/) +into [Bulgur Cloud](https://github.com/SeriousBug/bulgur-cloud) and I hit an +issue when trying to use a path as a parameter. + +What I wanted to do was to have a route where the rest of the path in the route +would be a paramete. For example, I can do this in my backend: + +```rust +#[get("/s/{store}/{path:.*}")] +pub async fn get_storage(// ... +``` + +This route will match all paths like `/s/user/`, as well as `/s/user/foo/` and +`/s/user/foo/bar.txt`. The key is that the path portion is a path with an +arbitrary number of segments. + +Unfortunately there doesn't seem to be built-in support for this in React +Navigation. Here's what I had at first: + +```ts +export type RoutingStackParams = { + Login: undefined; + Dashboard: { + store: string; + path: string; + }; +}; +export const Stack: any = createNativeStackNavigator(); +export const LINKING = { + prefixes: ["bulgur-cloud://"], + config: { + screens: { + Login: "", + Dashboard: "s/:store/", // Can't do "s/:store/*" or something like that + }, + }, +}; + + +function App() { + return ( + + + + + + + ); +} +``` + +This would cause the URLs for the `Dashboard` to look like `/s/user/?path=file`. + +I read through all the docs, looked up many examples, and scoured any +Stackoverflow answers I could find. Nope, nobody seems to be talking about this. +This feels like such a fundamental piece of routing tech to me that I'm shocked +that not only is there no built-in support, nobody seems to be questioning why +it doesn't exist. + +Thankfully some folks in the [Reactiflux](https://www.reactiflux.com/) discord +pointed me towards the right way: using +[`getStateFromPath`](https://reactnavigation.org/docs/navigation-container#linkinggetstatefrompath) +and +[`getPathFromState`](https://reactnavigation.org/docs/navigation-container#linkinggetpathfromstate) +to write a custom formatter and parser for the URL. + +This is made easier thanks to the fact that you can still import and use the +built-in formatter and parser, and just handle the cases that you need to. +Here's what I implemented: + +```ts +export const LINKING = { + prefixes: ["bulgur-cloud://"], + config: { + screens: { + Login: "", + Dashboard: "s/:store/", + }, + }, + getStateFromPath: (path: string, config: any) => { + // For the Dashboard URLs only... + if (path.startsWith("/s/")) { + const matches = + // ...parse the URL as /s/:store/...path + /^[/]s[/](?[^/]+)[/](?.*)$/.exec( + path, + ); + const out = { + routes: [ + { + name: "Dashboard", + path, + params: { + store: matches?.groups?.store, + path: matches?.groups?.path, + }, + }, + ], + }; + return out; + } + // For all other URLs fall back to the built-in + const state = getStateFromPath(path, config); + return state; + }, + getPathFromState: (state: any, config: any) => { + const route = state.routes[0]; + // For the Dashboard routes only... + if (route?.name === "Dashboard") { + // ...directly put the path into the URL + const params: RoutingStackParams["Dashboard"] = route.params; + return `/s/${params.store}/${params.path}`; + } + // For all other routes fall back to the built-in + return getPathFromState(state, config); + }, +}; +``` + +I'm not sure how the get the types to be a little nicer, but just going with +`any` is fine for me in this case since it's a very small portion of the +codebase.