-
-
Save zhenghaohe/2cc21c1c275c06a6fc8e9c8f4895b5b4 to your computer and use it in GitHub Desktop.
useRequest
This file contains 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
type Status = "pending" | "resolved" | "rejected"; | |
type State<R, E = unknown> = { | |
status: Status; | |
data: null | R; | |
error: null | E; | |
}; | |
type Action<R, E = unknown> = | |
| { type: "pending" } | |
| { type: "resolved"; data: R } | |
| { type: "rejected"; error: E }; | |
// useSafeDispatch is for making sure that we do not dispatch actions after the component has unmounted, which creates memory leaks. | |
function useSafeDispatch<R, E = unknown>( | |
dispatch: React.Dispatch<Action<R, E>> | |
): React.Dispatch<Action<R, E>> { | |
const mountRef = useRef(false); | |
useLayoutEffect(() => { | |
mountRef.current = true; | |
return () => { | |
mountRef.current = false; | |
}; | |
}, []); | |
return useCallback( | |
(...args) => { | |
if (mountRef.current) dispatch(...args); | |
}, | |
[dispatch] | |
); | |
} | |
function createReducer<R, E>() { | |
return function reducer( | |
state: State<R, E>, | |
action: Action<R, E> | |
): State<R, E> { | |
switch (action.type) { | |
case "pending": { | |
return { status: "pending", data: null, error: null }; | |
} | |
case "resolved": { | |
return { status: "resolved", data: action.data, error: null }; | |
} | |
case "rejected": { | |
return { status: "rejected", data: null, error: action.error }; | |
} | |
default: { | |
throw new Error(`unhandled action type`); | |
} | |
} | |
}; | |
} | |
export default function useRequest<R, E = unknown>(intialState?: State<R, E>) { | |
const reducer = createReducer<R, E>(); | |
const [state, unsafeDispatch] = useReducer(reducer, { | |
status: "pending", | |
data: null, | |
error: null, | |
...intialState | |
}); | |
const safeDispatch = useSafeDispatch(unsafeDispatch); | |
const currentRequestRef = useRef<null | symbol>(null); | |
const request = useCallback( | |
// Wrap it inside useCallback so it has stable reference between re-renders | |
(promise: Promise<R>) => { | |
if (!promise || !promise.then) { | |
throw new Error("The argument must be a promise"); | |
} | |
// Taking advantage of the uniqueness of symbol and use it to mark the requests we have made | |
// This is for avoiding race conditions that might occur in rare cases | |
// e.g. when user makes two requests in a row and the second request comes back faster than the first request | |
// I could have used abort controller here to avoid the race condition but it has its own downsides. | |
const id = Symbol(); | |
currentRequestRef.current = id; | |
safeDispatch({ type: "pending" }); | |
promise.then( | |
(data: R) => { | |
if (currentRequestRef.current === id) | |
safeDispatch({ type: "resolved", data }); | |
}, | |
(error: E) => safeDispatch({ type: "rejected", error }) | |
); | |
}, | |
[safeDispatch] | |
); | |
return { | |
...state, | |
request | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment