Created
March 6, 2020 13:43
-
-
Save busypeoples/71a69e5514dd87cb770de6d39e325e3b to your computer and use it in GitHub Desktop.
RemoteData implementation using React, hooks and TypeScript
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 React, { useState } from "react"; | |
type NotAsked = { | |
_type: "NOT_ASKED"; | |
}; | |
type Loading = { | |
_type: "LOADING"; | |
}; | |
type Success<T> = { | |
_type: "SUCCESS"; | |
data: T; | |
}; | |
type Error<E> = { | |
_type: "ERROR"; | |
msg: E; | |
}; | |
type Data<T, E> = NotAsked | Loading | Error<E> | Success<T>; | |
type RemoteData<T, E> = { | |
NotAsked: NotAsked; | |
Loading: Loading; | |
Success: (data: T) => Success<T>; | |
Error: (msg: E) => Error<E>; | |
}; | |
const makeRemoteData = <T, E>(): RemoteData<T, E> => { | |
return { | |
NotAsked: { _type: "NOT_ASKED" }, | |
Loading: { _type: "LOADING" }, | |
Error: (msg: E) => ({ _type: "ERROR", msg }), | |
Success: (data: T) => ({ _type: "SUCCESS", data }) | |
}; | |
}; | |
type ViewInput<T, E> = { | |
notAsked: () => React.ReactNode; | |
loading: () => React.ReactNode; | |
error: (msg: E) => React.ReactNode; | |
success: (data: T) => React.ReactNode; | |
}; | |
const useRemoteData = <T, E>( | |
fetch: () => Promise<T> | |
): [(v: ViewInput<T, E>) => React.ReactNode, () => void] => { | |
const remoteData = makeRemoteData<T, E>(); | |
const [state, setState] = useState<Data<T, E>>(remoteData.NotAsked); | |
const loadData = () => { | |
setState(remoteData.Loading); | |
fetch().then( | |
data => setState(remoteData.Success(data)), | |
error => setState(remoteData.Error(error)) | |
); | |
}; | |
const view = ({ notAsked, loading, error, success }: ViewInput<T, E>) => { | |
switch (state._type) { | |
case "NOT_ASKED": | |
return notAsked(); | |
case "LOADING": | |
return loading(); | |
case "ERROR": | |
return error(state.msg); | |
case "SUCCESS": | |
return success(state.data); | |
} | |
}; | |
return [view, loadData]; | |
}; | |
// Example | |
const fakeFetch = (): Promise<User[]> => | |
new Promise((res, rej) => { | |
setTimeout( | |
() => | |
res([ | |
{ id: 1, name: "User A" }, | |
{ id: 2, name: "User B" }, | |
{ id: 3, name: "User C" } | |
]), | |
1000 | |
); | |
}); | |
type User = { | |
id: number; | |
name: string; | |
}; | |
const UserItems = ({ data }: { data: User[] }) => { | |
return ( | |
<div> | |
{data.map(user => { | |
return ( | |
<div key={user.id}> | |
{user.id}: {user.name} | |
</div> | |
); | |
})} | |
</div> | |
); | |
}; | |
const App = () => { | |
const [renderView, loadData] = useRemoteData<User[], string>(fakeFetch); | |
return ( | |
<div> | |
{renderView({ | |
notAsked: () => <div className="start">Not Data Loaded</div>, | |
loading: () => <div className="loader">Loading...</div>, | |
error: error => ( | |
<div className="error">Something went wrong: {error}</div> | |
), | |
success: data => <UserItems data={data} /> | |
})} | |
<button onClick={loadData}>Load Data</button> | |
</div> | |
); | |
}; | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You could handle NoDataView as well like here: https://gist.github.com/busypeoples/6e88899c81825964ac614cc2e71fc2f5