Skip to content

Conversation

jeyj0
Copy link

@jeyj0 jeyj0 commented Dec 13, 2024

This PR implements a new future flag: typesafeUseRouteLoaderData, which improves the type for useRouteLoaderData by making use of the new type generation infrastructure.

Note: I'm not sure if this affects library-mode. If it does, there's probably more work to be done.

Benefits

There are two benefits when enabling the flag:

Benefit 1: Type Inference

// instead of
const data = useRouteLoaderData<typeof rootLoader>("root");

// we just do
const data = useRouteLoaderData("root");

Benefit 2: Type Errors

// this throws a type-error if no route with id "doesnt_exist" exists
const data = useRouteLoaderData("doesnt_exist");

How does it work?

The current useRouteLoaderData type from its actual implementation is made impossible to reach, by requiring the parameter routeId to be of type never. The actual type is then declared using module augmentation within the user's .react-router/types directory. The actual generated type depends on the future flag.

When typesafeUseRouteLoaderData is disabled

Unfortunately, there is an additional generated file in .react-router/types for users who don't enable the flag (.react-router/types/useRouteLoaderData.d.ts). I'm not sure if that counts as changing the API. I wouldn't say so, but if you disagree I can also understand the reasoning behind that.

If the additional generated file counts as an API change, Benefit 2 will require a different approach to be implementable behind a future flag: namely, a type-level future flag via module augmentation. Benefit 1 (inference) can be implemented without that file in case the flag is disabled (if the parameter is not a RouteId, it would then fall back to the current type if nothing is done to enable Benefit 2).

When typesafeUseRouteLoaderData is enabled

react-router typegen generates two new files in .react-router/types when the future flag is enabled:

  • .react-router/types/routeManifest.d.ts exports two types: RouteManifest and RouteId. RouteManifest is an object type, which maps route ids to the Info export of that route's +types/... file. I chose to do this in a separate file, as these types might be useful for users as well.
  • .react-router/types/useRouteLoaderData.d.ts uses module augmentation to declare an new type for useRouteLoaderData, which uses the RouteManifest and RouteId types for .react-router/types/routeManifest.d.ts.

Copy link

changeset-bot bot commented Dec 13, 2024

🦋 Changeset detected

Latest commit: de8f6d7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@react-router/dev Major
react-router Major
@react-router/fs-routes Major
@react-router/remix-routes-option-adapter Major
@react-router/architect Major
@react-router/cloudflare Major
react-router-dom Major
@react-router/express Major
@react-router/node Major
@react-router/serve Major
create-react-router Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jeyj0 jeyj0 force-pushed the typesafe-useRouteLoaderData branch from 5649567 to cd237e3 Compare December 13, 2024 09:22
@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Dec 13, 2024

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

@jeyj0 jeyj0 force-pushed the typesafe-useRouteLoaderData branch from cd237e3 to 1bff006 Compare December 13, 2024 09:43
@jeyj0
Copy link
Author

jeyj0 commented Dec 13, 2024

I realized that I had accidentally deleted the undefined case in the new type at some point before committing.

Now everything should be good!

@remorses
Copy link
Contributor

I really like this idea

@jeyj0
Copy link
Author

jeyj0 commented Dec 13, 2024

I have done some more experimenting because there's a hack that I quite disliked in the solution I wrote above. I came up with a more generally-useful approach that could be used in the future, to type any feature that depends on the user's project (and thus can't be known in the react-router repository).

It still has the issue of a file being generated in .react-router/types for the case that the future flag is disabled. I'm not sure if it's possible to avoid that with my idea.

The core improvement of the new technique is generating a file in .react-router/types that declares a new module that doesn't actually exist, but is useful for types. I've called this module react-router/user-types:

// .react-router/types/react-router-user-types.d.ts
declare module "react-router/user-types" {
  // This is just one way of doing things, and currently references .react-router/types/routeManifest.d.ts as with the implementation above.
  // You could just as well include the code from `.react-router/types/routeManifest.d.ts` in this file as well.
  export interface UserTypes {
    routeManifest: import("./routeManifest").RouteManifest;
    routeId: import("./routeManifest").RouteId;
  }  
}

Additionally, for making the future flag work on the type-level too, I'm also generating .react-router/types/react-router-future.d.ts with the same approach (this file could also simply be merged with the above).

// .react-router/types/react-router-future.d.ts
declare module "react-router/future" {
  export interface TypeLevelFutureFlagConfig {
    typesafeUseRouteLoaderData: false; // or true, if the flag is enabled in react-router.config.ts
  }
}

This allows referencing these types within the react router repository. For useRouteLoaderData, that looks for example like this:

// in hooks.tsx
import type { TypeLevelFutureFlagConfig } from "react-router/future";
import type { UserTypes } from "react-router/user-types";

// ...

type UseRouteLoaderData =
  TypeLevelFutureFlagConfig["typesafeUseRouteLoaderData"] extends true
    // this is the type if the future flag is enabled
    ? <RI extends UserTypes["routeId"]>(
        routeId: RI
      ) => UserTypes["routeManifest"][RI]["loaderData"]
    // this is the type if the future flag is disabled (the default)
    : <T = any>(routeId: string) => SerializeFrom<T> | undefined;
export var useRouteLoaderData: UseRouteLoaderData = (routeId) => {
  let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
  return state.loaderData[routeId];
};

The react-router repo would just need a .d.ts file setting the defaults for a nicer experience during development. Since they aren't included in the user's tsconfig, they are ignored and don't affect the actual user types.

I'm sure the naming can be improved, and I'm not sure if the nesting into interfaces provides any advantages (they just resulted from my experimentation).

It might also be a good idea to provide empty runtime modules for "react-router/user-types" and "react-router/future" if users might import the modules by accident. Again, I'm not sure what the best approach for that would be.

But I think the concept is pretty clear.

This approach could also be used to deal with these issues, by making it possible to detect library vs. framework use on the type level: #12348 #12488

enable via typesafeUseRouteLoaderData future flag
@jeyj0 jeyj0 force-pushed the typesafe-useRouteLoaderData branch from 1bff006 to de8f6d7 Compare December 16, 2024 10:35
@jeyj0 jeyj0 marked this pull request as draft December 16, 2024 10:37
@pcattori
Copy link
Contributor

Hi @jeyj0, thanks for taking a deep dive into type-safety and exploring this option!

We're actually targeting a similar API soon for access to a route from other routes but it has a couple notable differences, so we won't be going with the exact API in this PR. Thanks again for your implementation though; even though its not the one we're going with its extremely validating to see such a similar idea from the community as it lets us know we're on track!

@pcattori pcattori closed this Jan 22, 2025
@einarq
Copy link

einarq commented Feb 19, 2025

Hi @jeyj0, thanks for taking a deep dive into type-safety and exploring this option!

We're actually targeting a similar API soon for access to a route from other routes but it has a couple notable differences, so we won't be going with the exact API in this PR. Thanks again for your implementation though; even though its not the one we're going with its extremely validating to see such a similar idea from the community as it lets us know we're on track!

So there is currently no way to get type-safe loaderData from the useRouteLoaderData hook?

@pcattori
Copy link
Contributor

Not yet

@geemanjs
Copy link

Hey @pcattori is there an issue to track this feature? I think with the .types generation this should be a lot easier to implement now?

@remorses
Copy link
Contributor

Any progress on type safe useRouteLoaderData?

@pcattori
Copy link
Contributor

Superceded by #13073

@einarq
Copy link

einarq commented Jun 18, 2025 via email

@remorses
Copy link
Contributor

@einarq

useRouterState()
By providing a path to the hook, we can match against it to determine the states, and even provide better types

@remorses
Copy link
Contributor

Given useRouteLoaderData will stay for a long time even if new routing APIs are added, can we add type safety to useRouteLoaderData? I can open a PR, will it be merged?

@pcattori
Copy link
Contributor

pcattori commented Sep 23, 2025

Given useRouteLoaderData will stay for a long time even if new routing APIs are added, can we add type safety to useRouteLoaderData? I can open a PR, will it be merged?

@remorses : I actually picked this back up this week, so its actively being worked on! So probably best to hold off on opening up your own PR to avoid duplicating work, but greatly appreciate the offer!

@remorses
Copy link
Contributor

awesome! this will make react-router so much better especially for passing down loader props type safely, thank you

@pcattori
Copy link
Contributor

pcattori commented Oct 1, 2025

Ok figured out a good approach and starting to work on implementation here: #14407

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants