Reputation: 51
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
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