Last active
February 2, 2024 14:09
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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