Luke Rogerson
Luke Rogerson

Reputation: 51

Does this make sense as a useApi-type hook? How do I properly type where it's used?

This is a hook I wrote to consume an existing "data loader" class that handles the authentication and data fetching itself, in order to expose things like loading flags and errors better.


export const useApi = (endpoint: string, options: object = {}) => {
    const [data, setData] = useState(null)
    const [isLoading, setIsLoading] = useState(false)
    const [error, setError] = useState(null)
    const loader = DataLoader.instance

    useEffect((): (() => void) | undefined => {
        if (!endpoint) {
            console.warn('Please include an endpoint!')
            return
        }
        let isMounted = true // check component is still mounted before setting state
        const fetchUserData = async () => {
            isMounted && setIsLoading(true)
            try {
                const res = await loader.load(endpoint, options)
                if (!res.ok) {
                    isMounted && setData(res)
                } else {
                    throw new Error()
                }
            } catch (error) {
                isMounted && setError(error)
            }
            isMounted && setIsLoading(false)
        }
        fetchUserData()
        return () => (isMounted = false)
    }, [endpoint])

    return { data, isLoading, error }
}

Does it make sense? I then use it like so:

const { data, isLoading, error } = useApi(Endpoints.user.me)

Assuming it's OK, how do I properly type the consumer? When I try to use a particular property on data, TypeScript will complain that "the Object is possibly null".

Many thanks in advance.

Upvotes: 0

Views: 90

Answers (1)

Skyrocker
Skyrocker

Reputation: 1049

At glance this hook seems reasonable. As for the proper typing here is when the TS Generics comes into play

import { useEffect, useState } from "react";

// This is the least example, what I assume the DataLoader should looks like.
// Here you need to declare the proper return type of the *load* function  
const DataLoader = {
  instance: {
    load: (endpoint, options) => {
      return {
        ok: true
      };
    }
  }
};

type ApiData = {
  ok: boolean;
};

export const useApi = <T extends ApiData>(
  endpoint: string,
  options: object = {}
) => {
  const [data, setData] = useState<T>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const loader = DataLoader.instance;

  useEffect((): (() => void) | undefined => {
    if (!endpoint) {
      console.warn("Please include an endpoint!");
      return;
    }
    let isMounted = true;
    const fetchUserData = async () => {
      isMounted && setIsLoading(true);
      try {
        // I used here *as* construction, because the *res* type should match with your *useState* hook type of data
        const res = (await loader.load(endpoint, options)) as T;  
        if (!res.ok) {
          isMounted && setData(res);
        } else {
          throw new Error();
        }
      } catch (error) {
        isMounted && setError(error);
      }
      isMounted && setIsLoading(false);
    };
    fetchUserData();
    return () => (isMounted = false);
  }, [endpoint]);

  return { data, isLoading, error };
};

But if you want to do this in the right way, you should also declare a DataLoader class (or whatever you have) with an ability to pass the Generic of the data type

Upvotes: 1

Related Questions