What type to declare for useLoaderData(), and where?
Asked Answered
K

4

10

I'm working on the NetNinja react-router in-depth #6 tutorial, but I'm trying to do it with typescript. I am fetching data from a JSON object, and I'm using the useLoaderData() hook and trying to map out the JSON object. I'm just not sure where to declare the type and/or which type to declare. Here is my component:

import { NavLink, Link, Outlet, useLoaderData } from 'react-router-dom'

type Token = {
  id: number
  name: string
  ticker: string
  description: string
}

const Tokens = () => { 
  const tokens = useLoaderData()

  return ( 
    <div>
      <div>
        {tokens.map((token: Token) => (
          <Link to='/' key={token.id}>
            <p>{token.name}</p>
            <p>{token.ticker}</p>
            <p>{token.description}</p>
          </Link>
        ))}
      </div>
      <nav>
        <NavLink to='uniswap'>Uniswap</NavLink>
        <NavLink to='balancer'>Balancer</NavLink>
      </nav>
      <Outlet />
    </div>
  );
}

//loader function
export const tokensLoader = async () => {
  const res = await fetch('http://localhost:4000/tokens')

  return res.json()
}
 
export default Tokens;

The JSON object:

{
  "tokens": [
    {
      "id": 1,
      "name": "uniswap",
      "ticker": "UNI",
      "description": "This is the description for Uniswap token"
    },
    {
      "id": 2,
      "name": "balancer",
      "ticker": "BAL",
      "description": "This is the description for Balancer token"
    }
  ]
}

My intuition felt like it should either be something like:

const tokens: Token[] = useLoaderData()

or

type TokenData = {
  tokens: Token[]
}

const tokens: TokenData = useLoaderData()

There is a lot going on and it is beyond my current understanding just following this tutorial. Which type should I be declaring and where?

Kristenkristi answered 15/3, 2023 at 3:20 Comment(0)
E
8

The return type of the useLoaderData hook is unknown.

See Source

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];
}

Just cast the return type to your TokenData type. Don't forget to destructure the tokens array that you'd like to map to JSX.

export type Token = {
  id: number;
  name: string;
  ticker: string;
  description: string;
};

export type TokenData = {
  tokens: Token[];
};
const { tokens } = useLoaderData() as TokenData;

Full example:

import { NavLink, Link, Outlet, useLoaderData } from "react-router-dom";
import { Token, TokenData } from "./types";

const Tokens = () => {
  const { tokens } = useLoaderData() as TokenData;

  return (
    <div>
      <div>
        {tokens.map((token: Token) => (
          <Link to="/" key={token.id}>
            <p>{token.name}</p>
            <p>{token.ticker}</p>
            <p>{token.description}</p>
          </Link>
        ))}
      </div>
      <nav>
        <NavLink to="uniswap">Uniswap</NavLink>
        <NavLink to="balancer">Balancer</NavLink>
      </nav>
      <Outlet />
    </div>
  );
};

Edit declaring-type-for-useloaderdata-which-type-to-declare-and-where

Excessive answered 15/3, 2023 at 4:28 Comment(0)
B
3

You can consider creating a type for useLoaderData and writing a wrapper for it:

// my-hooks/useLoaderData
import { useLoaderData as useLoaderDataOriginal } from 'react-router-dom'

interface UseLoaderDataHook {
  <T>():T
}

const useLoaderData:UseLoaderDataHook = () => {
  const useLoaderDataRef = useLoaderDataOriginal as UseLoaderDataHook
  return useLoaderDataRef()
}

Now you could use this hook in your component instead of original hook:

import {useLoaderData} from 'my-hooks/useLoaderData'

interface Car {
  id: string
  model: string
}

const CarComponent = () => {
    const car = useLoaderData<Car>() // use generic Car
    return <div>Car: {car.model}</div> // you have autocomplete here :)
}

Bowery answered 26/7, 2023 at 4:7 Comment(0)
G
0

I encountered the same issue and came up with a custom hook where I pass a loader function as an argunent to define ReturnType:

import { useLoaderData } from 'react-router-dom';

type TAnyFunction = (...args: any) => any;

export const useTypedLoaderData = <T extends TAnyFunction>(loaderFunction: T) => {
    return useLoaderData() as Awaited<ReturnType<typeof loaderFunction>>;
};

Here's how I use it:

const { contacts } = useTypedLoaderData(mainLayoutLoader);
Gasconade answered 10/6 at 9:4 Comment(0)
T
0

Create a useLoaderData.ts file in the hooks folder:

import { useLoaderData as useLoaderDataOrig } from 'react-router-dom';

export function useLoaderData<T = unknown>(): T {
  return useLoaderDataOrig() as T;
}
Triumphant answered 6/9 at 18:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.