Skip to content

Instantly share code, notes, and snippets.

@honzabrecka
Last active July 22, 2020 17:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save honzabrecka/e8ab1c0c36e53490fe48d0908309c564 to your computer and use it in GitHub Desktop.
Save honzabrecka/e8ab1c0c36e53490fe48d0908309c564 to your computer and use it in GitHub Desktop.
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