3.9 KiB
title | date | draft | toc | images | tags | ||||
---|---|---|---|---|---|---|---|---|---|
Using a path as a parameter in React Navigation | 2022-05-01T17:49:02-04:00 | true | false |
|
I've been trying to integrate React Navigation into 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 parameter. For example, I can do this in my backend:
#[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:
export type RoutingStackParams = {
Login: undefined;
Dashboard: {
store: string;
path: string;
};
};
export const Stack: any = createNativeStackNavigator<RoutingStackParams>();
export const LINKING = {
prefixes: ["bulgur-cloud://"],
config: {
screens: {
Login: "",
Dashboard: "s/:store/", // Can't do "s/:store/*" or something like that
},
},
};
function App() {
return (
<NavigationContainer linking={LINKING}>
<Stack.Navigator initialRouteName="Login">
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Dashboard" component={Dashboard} />
</Stack.Navigator>
</NavigationContainer>
);
}
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 discord
pointed me towards the right way: using
getStateFromPath
and
getPathFromState
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:
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[/](?<store>[^/]+)[/](?<path>.*)$/.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.