Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(remix-react): add types to useRouteLoaderData #5157

Merged
merged 2 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/type-userouteloaderdata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

Add generic type for `useRouteLoaderData()`
57 changes: 56 additions & 1 deletion packages/remix-react/__tests__/hook-types-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,126 @@ import type {
TypedResponse,
} from "@remix-run/server-runtime";

import type { useLoaderData } from "../components";
import type { useLoaderData, useRouteLoaderData } from "../components";

function isEqual<A, B>(
arg: A extends B ? (B extends A ? true : false) : false
): void {}

type LoaderData<T> = ReturnType<typeof useLoaderData<T>>;
type RouteLoaderData<T> = ReturnType<typeof useRouteLoaderData<T>>;

describe("useLoaderData", () => {
it("supports plain data type", () => {
type AppData = { hello: string };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports plain Response", () => {
type Loader = (args: any) => Response;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, any>(true);
isEqual<response, routeResponse>(true);
});

it("infers type regardless of redirect", () => {
type Loader = (
args: any
) => TypedResponse<{ id: string }> | TypedResponse<never>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { id: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports Response-returning loader", () => {
type Loader = (args: any) => TypedResponse<{ hello: string }>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports async Response-returning loader", () => {
type Loader = (args: any) => Promise<TypedResponse<{ hello: string }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports data-returning loader", () => {
type Loader = (args: any) => { hello: string };
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports async data-returning loader", () => {
type Loader = (args: any) => Promise<{ hello: string }>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});
});

describe("type serializer", () => {
it("converts Date to string", () => {
type AppData = { hello: Date };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, { hello: string }>(true);
isEqual<response, routeResponse>(true);
});

it("supports custom toJSON", () => {
type AppData = { toJSON(): { data: string[] } };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, { data: string[] }>(true);
isEqual<response, routeResponse>(true);
});

it("supports recursion", () => {
type AppData = { dob: Date; parent: AppData };
type SerializedAppData = { dob: string; parent: SerializedAppData };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, SerializedAppData>(true);
isEqual<response, routeResponse>(true);
});

it("supports tuples and arrays", () => {
type AppData = { arr: Date[]; tuple: [string, number, Date]; empty: [] };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<
response,
{ arr: string[]; tuple: [string, number, string]; empty: [] }
>(true);
isEqual<response, routeResponse>(true);
});

it("transforms unserializables to null in arrays", () => {
type AppData = [Function, symbol, undefined];
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, [null, null, null]>(true);
isEqual<response, routeResponse>(true);
});

it("transforms unserializables to never in objects", () => {
type AppData = { arg1: Function; arg2: symbol; arg3: undefined };
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, {}>(true);
isEqual<response, routeResponse>(true);
});

it("supports class instances", () => {
Expand All @@ -105,7 +132,9 @@ describe("type serializer", () => {
}
type Loader = (args: any) => TypedResponse<Test>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { arg: string }>(true);
isEqual<response, routeResponse>(true);
});

it("makes keys optional if the value is undefined", () => {
Expand All @@ -115,7 +144,9 @@ describe("type serializer", () => {
arg3: undefined;
};
type response = LoaderData<AppData>;
type routeResponse = RouteLoaderData<AppData>;
isEqual<response, { arg1: string; arg2?: number }>(true);
isEqual<response, routeResponse>(true);
});

it("allows data key in value", () => {
Expand All @@ -131,23 +162,29 @@ describe("deferred type serializer", () => {
args: any
) => TypedDeferredData<{ hello: string; lazy: Promise<string> }>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string; lazy: Promise<string> }>(true);
isEqual<response, routeResponse>(true);
});

it("supports asynchronous loader", () => {
type Loader = (
args: any
) => Promise<TypedDeferredData<{ hello: string; lazy: Promise<string> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string; lazy: Promise<string> }>(true);
isEqual<response, routeResponse>(true);
});

it("supports synchronous loader with deferred object result", () => {
type Loader = (
args: any
) => TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string; lazy: Promise<{ a: number }> }>(true);
isEqual<response, routeResponse>(true);
});

it("supports asynchronous loader with deferred object result", () => {
Expand All @@ -157,7 +194,9 @@ describe("deferred type serializer", () => {
TypedDeferredData<{ hello: string; lazy: Promise<{ a: number }> }>
>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string; lazy: Promise<{ a: number }> }>(true);
isEqual<response, routeResponse>(true);
});

it("converts Date to string", () => {
Expand All @@ -167,7 +206,9 @@ describe("deferred type serializer", () => {
TypedDeferredData<{ hello: Date; lazy: Promise<{ a: Date }> }>
>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: string; lazy: Promise<{ a: string }> }>(true);
isEqual<response, routeResponse>(true);
});

it("supports custom toJSON", () => {
Expand All @@ -178,10 +219,12 @@ describe("deferred type serializer", () => {
TypedDeferredData<{ hello: AppData; lazy: Promise<{ a: AppData }> }>
>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: { data: string[] }; lazy: Promise<{ a: { data: string[] } }> }
>(true);
isEqual<response, routeResponse>(true);
});

it("supports recursion", () => {
Expand All @@ -191,10 +234,12 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: AppData; lazy: Promise<AppData> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: SerializedAppData; lazy: Promise<SerializedAppData> }
>(true);
isEqual<response, routeResponse>(true);
});

it("supports tuples and arrays", () => {
Expand All @@ -208,10 +253,12 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: AppData; lazy: Promise<AppData> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: SerializedAppData; lazy: Promise<SerializedAppData> }
>(true);
isEqual<response, routeResponse>(true);
});

it("transforms unserializables to null in arrays", () => {
Expand All @@ -221,10 +268,12 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: AppData; lazy: Promise<AppData> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: SerializedAppData; lazy: Promise<SerializedAppData> }
>(true);
isEqual<response, routeResponse>(true);
});

it("transforms unserializables to never in objects", () => {
Expand All @@ -233,7 +282,9 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: AppData; lazy: Promise<AppData> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<response, { hello: {}; lazy: Promise<{}> }>(true);
isEqual<response, routeResponse>(true);
});

it("supports class instances", () => {
Expand All @@ -245,10 +296,12 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: Test; lazy: Promise<Test> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: { arg: string }; lazy: Promise<{ arg: string }> }
>(true);
isEqual<response, routeResponse>(true);
});

it("makes keys optional if the value is undefined", () => {
Expand All @@ -262,10 +315,12 @@ describe("deferred type serializer", () => {
args: any
) => Promise<TypedDeferredData<{ hello: AppData; lazy: Promise<AppData> }>>;
type response = LoaderData<Loader>;
type routeResponse = RouteLoaderData<Loader>;
isEqual<
response,
{ hello: SerializedAppData; lazy: Promise<SerializedAppData> }
>(true);
isEqual<response, routeResponse>(true);
});

it("allows data key in value", () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
useFetchers as useFetchersRR,
useActionData as useActionDataRR,
useLoaderData as useLoaderDataRR,
useRouteLoaderData as useRouteLoaderDataRR,
useMatches as useMatchesRR,
useLocation,
useNavigation,
Expand Down Expand Up @@ -1271,6 +1272,15 @@ export function useLoaderData<T = AppData>(): SerializeFrom<T> {
return useLoaderDataRR() as SerializeFrom<T>;
}

/**
* Returns the loaderData for the given routeId.
*
* @see https://remix.run/hooks/use-route-loader-data
*/
export function useRouteLoaderData<T = AppData>(routeId: string): SerializeFrom<T> | undefined {
return useRouteLoaderDataRR(routeId) as SerializeFrom<T> | undefined;
}

/**
* Returns the JSON parsed data from the current route's `action`.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export {
useResolvedPath,
useRevalidator,
useRouteError,
useRouteLoaderData,
useSearchParams,
useSubmit,
unstable_useBlocker,
Expand All @@ -60,6 +59,7 @@ export {
useFetcher,
useFetchers,
useLoaderData,
useRouteLoaderData,
useMatches,
useActionData,
RemixContext as UNSAFE_RemixContext,
Expand Down
Loading