bgenc.net/content/posts/2022.05.01.react-navigation-path-as-parameter.md

4.3 KiB

title date lastmod draft toc images tags
Using a path as a parameter in React Navigation 2022-05-01T17:49:02-04:00 2022-05-09T04:28:00-04:00 true false
dev
react
javascript
typescript

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) => {
    // Getting the "top route" if we're using a stack navigator
    const route = state.routes[state.routes.length - 1];
    // 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.

Edit: Made the following change after noticing that this code didn't work with a stack navigator

// Getting the "top route" if we're using a stack navigator
const route = state.routes[state.routes.length - 1];