Last active
July 23, 2020 13:14
-
-
Save honzabrecka/9debf402fd4bc5001a2b3ef23ff3e8a6 to your computer and use it in GitHub Desktop.
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, { | |
Suspense, | |
useCallback, | |
useState, | |
useRef, | |
useEffect, | |
unstable_useTransition, | |
} from "react"; | |
const delay = (d) => new Promise((res) => setTimeout(res, d)); | |
/// FAKES | |
let users = { | |
"/user/1": { | |
id: 1, | |
firstname: "Alfons", | |
lastname: "Baar", | |
}, | |
"/user/2": { | |
id: 2, | |
firstname: "Charles", | |
lastname: "Leclerc", | |
}, | |
"/user/3": { | |
id: 3, | |
firstname: "Eddy", | |
lastname: "Foobar", | |
}, | |
}; | |
const fakeFetch = (url, options) => { | |
console.log("fakeFetch", url, options); | |
if (options) { | |
users[url] = options.body; | |
} | |
return delay(1500).then(() => users[url]); | |
}; | |
/// LIB | |
function wrapPromise(x, lazy) { | |
const [rejected, resolved, pending] = [0, 1, 2]; | |
let state = [pending, undefined]; | |
let promise; | |
function start() { | |
promise = x(); | |
promise | |
.then((data) => { | |
state = [resolved, data]; | |
}) | |
.catch((error) => { | |
state = [rejected, error]; | |
}); | |
} | |
if (!lazy) start(); | |
return { | |
read() { | |
if (!promise) start(); | |
const [status, result] = state; | |
if (status == pending) throw promise; | |
if (status == rejected) throw result; | |
return result; | |
}, | |
write(value) { | |
state = [resolved, value]; | |
}, | |
}; | |
} | |
const useResource = (factory, options, params) => { | |
const [startTransition, isPending] = unstable_useTransition({ | |
timeoutMs: options.optimisticUpdateTimeout || 1000, | |
}); | |
const [resource, setResource] = useState(() => factory(params)); | |
const refresh = useCallback(() => { | |
startTransition(() => { | |
setResource(factory(params)); | |
}); | |
}, [factory]); | |
const update = useCallback( | |
(value) => { | |
resource.write && resource.write(value); | |
startTransition(() => { | |
setResource(factory(params, value)); | |
}); | |
}, | |
[factory] | |
); | |
return [resource, refresh, update, isPending]; | |
}; | |
const createResource = ({ read, write, lazy = false }) => (params, value) => { | |
return wrapPromise(() => (value ? write(params, value) : read(params)), lazy); | |
}; | |
const createPaginatedResource = ({ read }) => () => { | |
return { | |
read() {}, | |
next() {}, | |
isNextLoading() {}, | |
hasNext() {}, | |
}; | |
}; | |
/// APP | |
const Users = ({ resource, refresh }) => { | |
const users = resource.read(); | |
return ( | |
<> | |
<button onClick={refresh}>refresh</button> | |
<ul> | |
{Object.entries(users).map(([key, { firstname, lastname }]) => ( | |
<li key={key}> | |
{firstname} {lastname} | |
</li> | |
))} | |
</ul> | |
</> | |
); | |
}; | |
const User = ({ resource, update, isPending }) => { | |
const { firstname, lastname } = resource.read(); | |
const onClick = useCallback(() => { | |
update({ firstname: "Johan", lastname }); | |
}, [update, lastname]); | |
return ( | |
<ul> | |
<li | |
style={{ background: isPending ? "#CCC" : "#FFF" }} | |
onClick={isPending ? undefined : onClick} | |
> | |
{firstname} | |
</li> | |
<li>{lastname}</li> | |
</ul> | |
); | |
}; | |
const WithResource = ({ | |
factory, | |
fallback, | |
children, | |
optimisticUpdateTimeout, | |
...params | |
}) => { | |
const [resource, refresh, update, isPending] = useResource( | |
factory, | |
{ optimisticUpdateTimeout }, | |
params | |
); | |
return ( | |
<Suspense fallback={fallback}> | |
{typeof children === "function" | |
? children({ resource, refresh, update, isPending }) | |
: React.cloneElement(React.Children.only(children), { | |
resource, | |
refresh, | |
update, | |
isPending, | |
})} | |
</Suspense> | |
); | |
}; | |
const App = () => { | |
const usersResourceFactory = useCallback( | |
createResource({ | |
read() { | |
return delay(1500).then(() => ({ ...users })); | |
}, | |
}), | |
[] | |
); | |
const userResourceFactory = useCallback( | |
createResource({ | |
read: ({ id }) => { | |
return fakeFetch(`/user/${id}`); | |
}, | |
write: async ({ id }, value) => { | |
await fakeFetch(`/user/${id}`, { body: value }); | |
return fakeFetch(`/user/${id}`); | |
}, | |
}), | |
[] | |
); | |
return ( | |
<> | |
<WithResource fallback={"loading..."} factory={usersResourceFactory}> | |
<Users /> | |
</WithResource> | |
<h2>#1</h2> | |
<WithResource | |
fallback={"loading..."} | |
factory={userResourceFactory} | |
id={1} | |
> | |
<User /> | |
</WithResource> | |
<WithResource | |
fallback={"loading..."} | |
factory={userResourceFactory} | |
id={2} | |
optimisticUpdateTimeout={4000} | |
> | |
{({ resource, update, isPending }) => { | |
return ( | |
<> | |
<h2>#2 a</h2> | |
<Suspense fallback="loading..."> | |
<User | |
resource={resource} | |
update={update} | |
isPending={isPending} | |
/> | |
</Suspense> | |
<h2>#2 a</h2> | |
<Suspense fallback="loading..."> | |
<User | |
resource={resource} | |
update={update} | |
isPending={isPending} | |
/> | |
</Suspense> | |
</> | |
); | |
}} | |
</WithResource> | |
<h2>#3</h2> | |
<WithResource | |
fallback={"loading..."} | |
factory={userResourceFactory} | |
id={3} | |
> | |
<User /> | |
</WithResource> | |
</> | |
); | |
}; | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment