Last active
July 22, 2020 17:06
-
-
Save honzabrecka/e8ab1c0c36e53490fe48d0908309c564 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 } from "react"; | |
import { RecoilRoot, atomFamily, selectorFamily, useRecoilState } from "recoil"; | |
/// 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 delay = (d) => new Promise((res) => setTimeout(res, d)); | |
const fakeFetch = (url, options) => { | |
console.log("fakeFetch", url, options); | |
if (options) { | |
users[url] = options.body; | |
} | |
return delay(1500).then(() => users[url]); | |
}; | |
/// LIB data layer | |
const getAtomId = ({ key }) => { | |
const [, resourceId] = key.split("__"); | |
const id = resourceId | |
.slice(1, resourceId.length - 1) | |
.split("/") | |
.reverse()[0]; | |
return id; | |
}; | |
const defaultWrite = async () => {}; | |
const defaultGetAfterWrite = (local, remote, meta) => ({ | |
data: local, | |
meta, | |
}); | |
export const createResource = ({ | |
key, | |
read, | |
write = defaultWrite, | |
getAfterWrite = defaultGetAfterWrite, | |
refreshAfterWrite = false, | |
meta = null, | |
}) => { | |
const $resource = atomFamily({ | |
key: `resource:atom/mutable/${key}`, | |
default: undefined, | |
effects_UNSTABLE: [ | |
({ node, setSelf, onSet, getSnapshot }) => { | |
const id = getAtomId(node); | |
console.log("id:", id, node.key); | |
setSelf({ | |
// internal resource id/version | |
version: 0, | |
// optimistic update | |
dirty: false, | |
// data (local/remote) | |
data: read(id, meta), | |
// meta | |
meta, | |
}); | |
onSet(async (newValue, oldValue) => { | |
if (newValue.version !== oldValue.version) { | |
const dataFromWrite = await write(id, newValue.data, newValue.meta); | |
const data = refreshAfterWrite | |
? await read(id, newValue.meta) | |
: dataFromWrite; | |
setSelf((state) => ({ | |
...state, | |
// in getAfterWrite we can update data/meta based on remote data | |
// or do nothing and return already updated state.data/meta | |
...getAfterWrite(state.data, data, newValue.meta), | |
version: state.version, | |
dirty: false, | |
})); | |
} | |
}); | |
}, | |
], | |
}); | |
const defaultId = "single"; | |
return selectorFamily({ | |
key: `resource:selector/mutable/${key}`, | |
get: (id = defaultId) => async ({ get }) => { | |
const resource = get($resource(`${key}/${id}`)); | |
return { | |
dirty: resource.dirty, | |
data: await resource.data, | |
}; | |
}, | |
set: (id = defaultId) => ({ get, set }, value = {}) => { | |
// value can be (to write | to refresh): | |
// { data?: any, meta?: any } | { refresh: boolean, meta?: any } | |
if (value.refresh) { | |
set($resource(`${key}/${id}`), (state) => { | |
const meta = value.meta || state.meta; | |
return { | |
...state, | |
meta, | |
data: read(id, meta), | |
}; | |
}); | |
} else { | |
set($resource(`${key}/${id}`), (state) => ({ | |
...state, | |
...value, | |
version: state.version + 1, | |
dirty: true, | |
})); | |
} | |
}, | |
}); | |
}; | |
export function useReadOnlyResource($resource) { | |
const [resource, set] = useRecoilState($resource); | |
const refresh = useCallback((meta) => { | |
set({ meta, refresh: true }); | |
}, []); | |
return [resource, refresh]; | |
} | |
export function useMutableResource($resource) { | |
const [resource, set] = useRecoilState($resource); | |
const refresh = useCallback((meta) => { | |
set({ meta, refresh: true }); | |
}, []); | |
return [resource, set, refresh]; | |
} | |
/// APP | |
const $remoteUsersResource = createResource({ | |
key: "users/remote", | |
read: (id, meta) => { | |
return delay(1500).then(() => ({ ...users })); | |
}, | |
}); | |
const $remoteUserResource = createResource({ | |
key: "user/remote", | |
read: (id, meta) => { | |
return fakeFetch(`/user/${id}`); | |
}, | |
write: (id, data, meta) => { | |
console.log("token:", meta.token); | |
return fakeFetch(`/user/${id}`, { method: "POST", body: data }); | |
}, | |
}); | |
const $remoteNonOptimisticUserResource = createResource({ | |
key: "user/non-optimistic", | |
read: (id, meta) => { | |
return fakeFetch(`/user/${id}`); | |
}, | |
write: async (id, data, meta) => { | |
return fakeFetch(`/user/${id}`, { method: "POST", body: meta }); | |
}, | |
refreshAfterWrite: true, | |
getAfterWrite: (local, remote, meta) => ({ data: remote, meta }), | |
}); | |
const $localUserResource = ((memory) => | |
createResource({ | |
key: "user/local", | |
read: async (id) => { | |
return memory[id]; | |
}, | |
write: async (id, data) => { | |
memory[id] = data; | |
}, | |
}))({ ...users }); | |
const User = ({ id }) => { | |
const [resource, setUser] = useMutableResource($remoteUserResource(id)); | |
console.log(resource); | |
// update | |
const onClick = useCallback(() => { | |
setUser({ | |
data: { ...resource.data, firstname: "Johan" }, | |
meta: { token: "xyz" }, | |
}); | |
}, [resource.data]); | |
return ( | |
<ul> | |
<li | |
style={{ background: resource.dirty ? "#CCC" : "#FFF" }} | |
onClick={onClick} | |
> | |
{resource.data.firstname} | |
</li> | |
<li>{resource.data.lastname}</li> | |
</ul> | |
); | |
}; | |
const UserNonOptimistic = ({ id }) => { | |
const [resource, setUser] = useMutableResource( | |
$remoteNonOptimisticUserResource(id) | |
); | |
console.log(resource); | |
// update | |
const onClick = useCallback(() => { | |
setUser({ | |
meta: { ...resource.data, firstname: "Johan" }, | |
}); | |
}, [resource.data]); | |
return ( | |
<ul> | |
<li | |
style={{ background: resource.dirty ? "#CCC" : "#FFF" }} | |
onClick={onClick} | |
> | |
{resource.data.firstname} | |
</li> | |
<li>{resource.data.lastname}</li> | |
</ul> | |
); | |
}; | |
const Users = () => { | |
const [resource, refresh] = useReadOnlyResource($remoteUsersResource()); | |
console.log(resource); | |
const onClick = useCallback(() => { | |
refresh(); | |
}, []); | |
return <button onClick={onClick}>refresh</button>; | |
}; | |
const App = () => { | |
return ( | |
<RecoilRoot> | |
<Suspense fallback={"loading..."}> | |
<Users /> | |
</Suspense> | |
<h2>#1</h2> | |
<Suspense fallback={"loading..."}> | |
<User id={1} /> | |
</Suspense> | |
<h2>#2 a</h2> | |
<Suspense fallback={"loading..."}> | |
<User id={2} /> | |
</Suspense> | |
<h2>#2 b</h2> | |
<Suspense fallback={"loading..."}> | |
<User id={2} /> | |
</Suspense> | |
<h2>#3</h2> | |
<Suspense fallback={"loading..."}> | |
<User id={3} /> | |
</Suspense> | |
<h2>#1 non optimistic</h2> | |
<Suspense fallback={"loading..."}> | |
<UserNonOptimistic id={1} /> | |
</Suspense> | |
</RecoilRoot> | |
); | |
}; | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment