Skip to content

Instantly share code, notes, and snippets.

@lauslim12
Last active February 2, 2024 14:09
Show Gist options
  • Save lauslim12/6d6704dfa3a207c014e1e14e0f133ede to your computer and use it in GitHub Desktop.
Save lauslim12/6d6704dfa3a207c014e1e14e0f133ede to your computer and use it in GitHub Desktop.
Naive data fetching hook that can possibly satisfy a lot of straightforward use-cases.
import { useCallback, useEffect, useMemo, useState } from "react";
/**
* State utilizes TypeScript's type system to our advantage. Having a discriminated union
* would make sense here because each state could have their own unique attributes. Being
* declarative here would make it more readable and easier to understand, compared to using
* imperative statements such as creating multiple states (`data`, `isLoading`, `error`) and
* then having them to rely on each other, which also doesn't work that well because of the
* TypeScript's type system being unable to understand dependant variables without making it
* to be a discriminated union.
*
* {@link https://redux.js.org/style-guide/#treat-reducers-as-state-machines}
* {@link https://kentcdodds.com/blog/make-impossible-states-impossible}
*/
type State<T> =
| { status: "idle" }
| { status: "pending" }
| { status: "success"; data: T }
| { status: "error"; error: unknown };
/**
* FetchArguments utilizes Fetch API's options and the URL as the properties.
*/
type FetchArguments = { url: string; options?: RequestInit };
/**
* Naive data fetching hook that can possibly satisfy a lot of straightforward use-cases. The
* implementation does not use `axios` and will rely on the browser's Fetch API. The generic
* `T` could be replaced with a more suitable `unknown` because sometimes we cannot be sure
* that the data that we receive are correct. The right way to assert the type is to use a
* schema parser library on the returned `state` variable and then asserting that the data
* shape that we expect is correct.
*
* @param options - The arguments in an object.
* @returns Instance of the state and the fetcher function to be recalled.
*/
export const useNaiveFetch = <T,>({ url, options }: FetchArguments) => {
const [state, setState] = useState<State<T>>({ status: "idle" });
const handleDataFetching = useCallback(() => {
setState({ status: "pending" });
fetch(url, options)
.then(async (response) => {
if (!response.ok) {
const error = await response.json();
throw error;
}
return response.json();
})
.then((data) => setState({ status: "success", data }))
.catch((error) => setState({ status: "error", error }));
}, [url, options]);
useEffect(() => {
handleDataFetching();
}, [handleDataFetching]);
// Make sure that the state and the function is cached.
const value = useMemo(
() => ({ state, fetchData: handleDataFetching }),
[state, handleDataFetching],
);
// Handling the loading state, success state, suspense, making sure that
// the data from the state conforms to the expected schema/type, and other
// implementation details are left as the parts that should be controlled
// and decided by the components themselves, so it is not included here.
//
// The destructuring is intentional, if `value` is returned as it is, then
// the hook will be infinite as it's always creating a new object on every return.
return { state: value.state, fetchData: value.fetchData };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment