Last active
June 29, 2022 14:06
-
-
Save jsamr/ac6f958003dae8053248be8450d1c79b to your computer and use it in GitHub Desktop.
Explicit Finite State Machine Pattern
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
import { useCallback, useEffect, useReducer } from "react"; | |
type FetchTransition<P> = | |
| { | |
type: "fetch"; | |
} | |
| { | |
type: "fetchOK"; | |
data: P; | |
} | |
| { | |
type: "fetchError"; | |
error: Error; | |
}; | |
type FetchState<P> = | |
| DryState | |
| DryErrorState | |
| FetchingState | |
| RefetchingState<P> | |
| FreshState<P> | |
| StaleState<P>; | |
interface DryState { | |
data: null; | |
error: null; | |
isLoading: false; | |
type: "dry"; | |
} | |
interface DryErrorState { | |
data: null; | |
error: Error; | |
isLoading: false; | |
type: "dryError"; | |
} | |
interface FetchingState { | |
data: null; | |
error: null; | |
isLoading: true; | |
type: "fetching"; | |
} | |
interface RefetchingState<P> { | |
data: P; | |
error: null; | |
isLoading: true; | |
type: "refetching"; | |
} | |
interface FreshState<P> { | |
data: P; | |
error: null; | |
isLoading: false; | |
type: "fresh"; | |
} | |
interface StaleState<P> { | |
data: P; | |
error: Error; | |
isLoading: false; | |
type: "stale"; | |
} | |
export const stateFactory = { | |
dry(): DryState { | |
return { | |
data: null, | |
error: null, | |
isLoading: false, | |
type: "dry", | |
}; | |
}, | |
dryError(error: Error): DryErrorState { | |
return { | |
data: null, | |
error, | |
isLoading: false, | |
type: "dryError", | |
}; | |
}, | |
fetching(): FetchingState { | |
return { | |
data: null, | |
error: null, | |
isLoading: true, | |
type: "fetching", | |
}; | |
}, | |
fresh<P>(data: P): FreshState<P> { | |
return { | |
data, | |
error: null, | |
isLoading: false, | |
type: "fresh", | |
}; | |
}, | |
refetching<P>(cachedData: P): RefetchingState<P> { | |
return { | |
data: cachedData, | |
error: null, | |
isLoading: true, | |
type: "refetching", | |
}; | |
}, | |
stale<P>(error: Error, cachedData: P): StaleState<P> { | |
return { | |
data: cachedData, | |
error, | |
isLoading: false, | |
type: "stale", | |
}; | |
}, | |
}; | |
export function fetchReducer<P>( | |
state: FetchState<P>, | |
transition: FetchTransition<P> | |
): FetchState<P> { | |
if (state.type === "dry" && transition.type === "fetch") { | |
return stateFactory.fetching(); | |
} | |
if (state.type === "dryError" && transition.type === "fetch") { | |
return stateFactory.fetching(); | |
} | |
if (state.type === "fetching" && transition.type === "fetchOK") { | |
return stateFactory.fresh(transition.data); | |
} | |
if (state.type === "fetching" && transition.type === "fetchError") { | |
return stateFactory.dryError(transition.error); | |
} | |
if (state.type === "fresh" && transition.type === "fetch") { | |
return stateFactory.refetching(state.data); | |
} | |
if (state.type === "refetching" && transition.type === "fetchOK") { | |
return stateFactory.fresh(transition.data); | |
} | |
if (state.type === "refetching" && transition.type === "fetchError") { | |
return stateFactory.stale(transition.error, state.data); | |
} | |
if (state.type === "stale" && transition.type === "fetch") { | |
return stateFactory.refetching(state.data); | |
} | |
console.warn( | |
`Unauthorized transition ${transition.type} while in state ${state.type}` | |
); | |
return state; | |
} | |
export default function useFetch<P>({ | |
url, | |
autoFetch, | |
}: { | |
url: string; | |
autoFetch: boolean; | |
}) { | |
const [state, dispatch] = useReducer(fetchReducer, void 0, stateFactory.dry); | |
const launchFetch = useCallback(() => { | |
if (!url) return; | |
dispatch({ type: "fetch" }); | |
}, [url]); | |
useEffect( | |
function runFetch() { | |
let canceled = false; | |
if (state.isLoading) { | |
(async () => { | |
try { | |
const response = await fetch(url); | |
if (canceled) return; | |
if (!response.ok) { | |
throw new Error(response.statusText); | |
} | |
const data = await response.json(); | |
if (canceled) return; | |
dispatch({ type: "fetchOK", data }); | |
} catch (error) { | |
if (canceled) return; | |
dispatch({ type: "fetchError", error }); | |
} | |
})(); | |
} | |
return () => { | |
canceled = true; | |
}; | |
}, | |
[url, state.isLoading] | |
); | |
useEffect(() => { | |
if (autoFetch) { | |
launchFetch(); | |
} | |
}, [autoFetch, launchFetch]); | |
return { state: state as FetchState<P>, fetch }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment