Reuseable api hook with type support

Viewed 62

I'm currently working on a project where I have an api module that contains the types and return types for all of the endspoints I use in the application. I'm seeing myself repeating a lot of logic when fetching data from my api, which is why I though of making a react-hook to make this a bit easier.

I'm thinking the first parameter should be the function to call, and the next one an array of arguments to pass to this function. Since the arguments each api function takes are different, I would love to infer the type of the arguments, based on the function passed as the first parameter. I would also love for the data that I return from the hook, to correspond to what function has been called.

For example if the endpoint returns Promise<User>, I would like the data to be of type User.

This is a minimal example of what I'm trying to accomplish, and it's in no working state since I've been messing around trying to get the typings to work.

How I imagine it to be working

function App() {
  const user = useApi(api.getUser, "123");

  if (user.loading) return <p>Loading...</p>;
  if (user.error) return <p>{user.error}</p>;

  return (
    <div className="App">
      <h1>Hello {user.data.name}</h1>
    </div>
  );
}

API

type User = {
  name: string;
  age: number;
};

function getUser(userId: string): Promise<User> {
  return Promise.resolve({ name: "Peter", age: 20 });
}

function getFamilyMembers(userId: string): Promise<User[]> {
  return Promise.resolve([
    { name: "Jackie", age: 17 },
    { name: "John", age: 42 }
  ]);
}

useApi-hook

const useApi = (apiCall: keyof typeof api, arguments: any[]) => {
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState("");
  const [data, setData] = React.useState();

  React.useEffect(() => {
    // Fetch data from api and set state
    apiCall(...arguments)
      .then(res => setData(res))
      .catch(err => setError("Something bad happened :("))
      .finally(() => setLoading(false));
  }, []);

  return {
    loading,
    error,
    data
  };
};

I also created the minimal example on codesandbox

1 Answers

I don't think this is 100% doable, at least not yet. You can get nice typing of data, at least.

What you're essentially asking for is a way to define apiCall such that it takes a function of some unknown number and types of parameters, and matching parameters. These would be generically variadic functions, which are not currently a thing in TypeScript (but they're coming).

The sticking point is the generic types, what you could do instead is use variadic functions and just eat the lack of type safety. That would look like...

export const useApi = useApiImpl;

function useApiImpl<TReturn>(apiCall: (...a: any[]) => Promise<TReturn>, defaultState: TReturn, ...args: any[]) {
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState("");
  const [data, setData] = React.useState(defaultState);

  React.useEffect(() => {
    // Fetch data from api and set state
    apiCall(...args)
      .then(res => setData(res))
      .catch(err => setError("Something bad happened :("))
      .finally(() => setLoading(false));
  }, []);

  return {
    loading,
    error,
    data
  };
};

apiCall becomes a function that takes some arguments and returns some sort of promise, and args is some set of arguments to pass along. You'll need to take an explicit default state (or otherwise constrain TReturn and add some casts) to get solid type inference on data and setData. Fair warning, my React isn't super strong so I'm only guessing that React.useState(defaultState) is correct.


Now, in theory, when generic variadic tuples types are available you'll instead be able to write useApiImpl as

function useApiImpl<TArgs extends unknown[], TReturn>(apiCall: (...a: TArgs[]) => Promise<TReturn>, defaultState: TReturn, ...args: TArgs[]) {
  // otherwise the same
};

I can't actually test that, so the declaration may be off from what actually ships, but basically you'll be able to bind TArgs to a collection generic types and then require apiCall take the same number and kinds of types (as arguments) as are passed to useApiImpl in args.

Related