I'm working on learning Remix with a simple application that fetches clips from a Twitch channel and displays them in a table. I'm running into a weird issue with Typescript while trying to fetch this data from the Twitch API and am unsure how to proceed.
I have my Index component set up to use a deferred value from the loader (as described here: https://beta.reactrouter.com/en/dev/guides/deferred) and it is sort of working - the loader is fetching and returning the correct information and I can see the loading state showing up and then disappearing when the loading is complete. Hooray! But Typescript is complaining about the actual returned type of the data.
Here is the code that is causing me difficulties:
// _index.tsx
type ClipsResponse = {
count: number;
clips: ClipDto[];
}
export async function loader({ request }: LoaderArgs) {
const url = new URL(request.url);
const broadcasterName = url.searchParams.get("broadcasterName");
// fetchClips is complicated but returns type Promise<ClipsResponse>
return defer({ clipData: fetchClips(broadcasterName) });
}
export default function IndexRoute() {
const data = useLoaderData<typeof loader>();
return (
<Suspense fallback={<LoadingSpinner />}>
<Await
resolve={data.clipData}
errorElement={<ClipTableError broadcasterName="" />}
children={(clipData) => (
<div
className={`flex flex-1 justify-center my-4 rounded-md bg-slate-50 border-slate-300 border h-full w-full`}
>
<ClipTable clips={clipData.clips} />
</div>
)}
/>
</Suspense>
);
}
// ClipTable.tsx
type ClipTableProps = {
clips: ClipDto[];
};
const ClipTable = ({ clips = [] }: ClipTableProps) => {
// do stuff to display clips in table
}
The error that I'm getting is when trying to pass clipData.clips
to the <ClipTable />
component; Typescript errors by saying:
Type 'SerializeObject<UndefinedToOptional<ClipDto>>[]' is not assignable to type 'ClipDto[]'.
Type 'SerializeObject<UndefinedToOptional<ClipDto>>' is not assignable to type 'ClipDto'.
I don't necessarily mind having to potentially parse whatever the loader is doing to the data to serialize it, but the documentation doesn't say anything about this type and I'm not sure where it's coming from or how to process it.
Any help would be appreciated!
The useLoaderData
hook doesn't work with generics and returns type unknown
.
See source:
/**
* Returns the loader data for the nearest ancestor Route loader
*/
export function useLoaderData(): unknown {
let state = useDataRouterState(DataRouterStateHook.UseLoaderData);
let routeId = useCurrentRouteId(DataRouterStateHook.UseLoaderData);
if (state.errors && state.errors[routeId] != null) {
console.error(
`You cannot \`useLoaderData\` in an errorElement (routeId: ${routeId})`
);
return undefined;
}
return state.loaderData[routeId];
}
You can cast the data
to the correct type. Typically something like:
const data = useLoaderData() as ClipsResponse;
Things get a little more heavy when deferring the response though, as the these get wrapped in guarded Promises. Recast the resolved/deferred value back to unknown
and then to your ClipsResponse
type.
I believe the following could be close to what you are looking for.
type ClipsResponse = {
count: number;
clips: ClipDto[];
}
...
export default function IndexRoute() {
const data = useLoaderData();
return (
<Suspense fallback={<LoadingSpinner />}>
<Await
resolve={data.clipData}
errorElement={<ClipTableError broadcasterName="" />}
children={(data) => {
const clipData = data as unknown as ClipsResponse; // <-- cast here
return (
<div
className="flex fl...ll w-full"
>
<ClipTable clips={clipData.clips} />
</div>
);
}}
/>
</Suspense>
);
}