justsift / resift Goto Github PK
View Code? Open in Web Editor NEWA state management library for data fetches in React
Home Page: https://resift.org/
License: MIT License
A state management library for data fetches in React
Home Page: https://resift.org/
License: MIT License
One of the hardest problems about data fetching in components is dealing with null values gracefully.
I originally thought suspense would be our silver bullet to everything against falsy data checks but that's not exactly the case. The issue with Suspense is that if a certain data requirement is not met, the whole component will be blocked from rendering. This not ideal in situations where you rely on a certain part of the component to be mounted for animations or layout structure and size.
For example, let's say you have a material-ui Drawer
component. This component should always be mounted regardless if there is data ready because of the animation in and the animation out. We shouldn't block the whole component because then we wouldn't be able to see the drawer animate in.
You could argue that we should just split up the components so that there is a suspending part and a non-suspending part but, imo, that division feels slightly clunky.
e.g.
import React from 'react';
import { useDispatch } from 'resift';
function SuspendingComponent({ exampleFetch }) {
const dispatch = useDispatch();
const data = useSuspendingFetch(exampleFetch, () => dispatch(exampleFetch()));
return <div>{data.stuff}</div>
}
function NonSuspendingComponent({ id }) {
const exampleFetch = makeExampleFetch(id);
return <Drawer>
<Suspense fallback={<CircularProgress />}>
<SuspendingComponent />
</Suspense>
</Drawer>;
}
It feels especially clunky having to pass some sort of prop down to the suspending component.
Anyway, I don't think that'll work so let's try something else.
This issue proposes: useGuard
useGuard
is a custom hook that takes in a fetch and returns a component that takes in a render prop. That render prop will only be invoked when there is data available and will provide the data as its argument.
e.g.
function UseGuardExample({ id }) {
const exampleFetch = makeExampleFetch(id);
const status = useFetch(exampleFetch)[1];
const Guard = useGuard(exampleFetch);
return <Drawer>
{isLoading(status) && <CircularProgress />}
<Guard>{data => <div>{data.stuff}</div>}</Guard>
</Drawer>;
}
Implementation:
import { useMemo } from "react";
import { useFetch, isNormal } from "resift";
function useGuard(fetch) {
return useMemo(() => {
function Guard({ children }) {
const [data, status] = useFetch(fetch);
return isNormal(status) && children(data);
}
return Guard;
}, [fetch]);
}
export default useGuard;
@pearlzhuzeng had experienced an unexpected result regarding the LOADING
status when developing the ReSift Rentals demo.
The demo is application is structured like Netflix's list of lists of movies. She had a few fetches that we were shared and they were also using the newer "merges across namespaces" feature.
The issue and question is: what is the status of a fetch that has a share across a namespace?
Here's the scenario, you have three fetches with two different namespaces:
makeGetGenre
- a fetch factory that gets a list of movies. namespace: genre
makeGetMovie
- a fetch factory that gets a single movie. namespace: movie
makeUpdateMovie
- a fetch factory that updates a single movie. namespace: movie
makeGetGenre
implements a merge for the namespace movie
so that when a movie item changes, the genre list changes to reflect that change.
The question is, what is the status of a genre when a movie is inflight?
// after you dispatch an update...
const updateMovie = makeUpdateMovie(movieId);
dispatch(updateMovie());
// ...what is the status of the genre list?
const getGenre = makeGetGenre(genreId);
const status = useStatus(getGenre);
// what should this be??
console.log(isLoading(status));
Through the current implementation, the output of isLoading
is true
.
With this behavior, whenever any movie goes inflight, it will cause all lists to have the status LOADING
. This is because the genre namespace doesn't know which movie belongs to which genre (and you can't know ahead of time).
This can be confusing to the developer because an update to a movie in a different genre would cause all the genres to update (which is what @pearlzhuzeng experienced). I think this behavior should not be the default and we should add other behavior options.
The proposal is to remove the option isolatedStatus
and replace it with type
.
The possible types of values of type
will be:
self
- only consider the status of the current fetch factory + keynamespace
- only consider the statuses of all fetches related the current namespace + keyall
- consider the statuses of all the fetches related to the current namespace + key as well as any other namespace (e.g. genre
namespace and movie
).const selfStatus = useStatus(getGenre, { type: 'self' }); // this is the new `isolatedStatus: true`
const namespaceStatus = useStatus(getGenre, { type: 'namespace' });
const allStatus = useStatus(getGenre, { type: 'all' });
I think the default type for useStatus
should be namespace
.
That's it! Let me know if you need any further clarification. I think @pearlzhuzeng knows what I'm talking about though.
TIL that suspense for data fetching is sort of working in React already by simply throwing a promise.
It's undocumented but looking at react-cache
(the experimental library demoed at React Conf 2018) shows that it works by throwing a promise.
And I've also created this demo as a quick POC of the mechanism.
I think this is a good indication of how suspense will work in the future and we can probably start brain storming resift APIs given this mechanism.
Current state:
We currently do not suspend rendering if data isn't present. Instead useFetch
returns null
for the data and UNKNOWN
for the status and it's up to the component to do the guard against null data as well as the initial dispatching to kick off the flow.
import React, { useEffect } from 'react';
import { useFetch, isNormal, useDispatch } from 'resift';
import makeMyFetch from './makeMyFetch';
function MyComponent({ id }) {
const dispatch = useDispatch();
const myFetch = makeMyFetch(id);
const [data, status] = useFetch();
useEffect(() => {
dispatch(myFetch());
}, [myFetch]);
// prevent rendering if no data
if (!isNormal(status) {
return <Spinner />
}
// when there is data then render...
return <div>{data.stuff}</div>
}
Possible next state:
We can create a new custom hook that will suspend rendering if the status does not contain NORMAL
. This new hook will need to throw a promise that when resolved, will be sufficient to tell Suspense the component is ready to be rendered again without the throw.
I'm imagining something like this:
function useSuspendingFetch(fetch, dataReadyNotifier) {
const result = useFetch(fetch);
const [data, status] = result;
if (!isNormal(status)) {
throw dataReadyNotifier();
}
return result;
}
function MyComponent({ id }) {
const myFetch = makeMyFetch(id);
const dispatch = useDispatch();
// data will never be `null` because this component will be Suspended in that case
const [data, status] = useSuspendingFetch(myFetch, () => dispatch(myFetch()));
return <div>{data.stuff}</div>;
}
Interestingly enough, this simple thing works right now!
This works because:
dispatch
returns a promise when a request is dispatcheduseSuspendingFetch
throw
s that promise that React.Suspense
will catch and awaitNORMAL
and the data will be presentSo that's that. Let me know what y'all think!
I think it would be cool to ship an unstable_useSuspendingFetch
in one of our upcoming releases.
I have an issue with the naming of our helper functions isNormal
, isLoading
etc.
The prefix is
is where I have the issue. The function isNormal
does not check if the status is exactly normal, instead checks to see if that status contains normal.
So let's rename it? π€
isNormal
=> hasNormal
function Component() {
const [data, status] = useFetch(myFetch):
return <>
{hasLoading(status) && <Spinner />}
{hasNormal(status) && <>{data.stuff}</>}
</>;
}
I think this helps communicate to our users that statuses are a collection of booleans vs a mutually exclusive state.
Manipulating a status (to force a certain status for the loader for example), currently requires some knowledge about bitwise operators which not great. Instead it would be better if we has manipulation methods like addNormal
, removeNormal
or similar.
Alternatively we could add a function addStatus(status, NORMAL)
to reduce the API footprint.
One of the issues with the current API of ReSift is that it's ambiguous which merge
function will run when two fetch factories implement a merge from one namespace
For example:
getMovies.js
import { defineFetch } from 'resift';
const makeGetMovies = defineFetch({
displayName: 'Get Movies',
share: { namespace: 'movieList' },
make: /* ... */,
});
const getMovies = makeGetMovies();
export default getMovies;
makeGetMovie.js
import { defineFetch } from 'resift';
const makeGetMovie = defineFetch({
displayName: 'Get Movie',
share: {
namespace: 'movieItem',
merge: {
// this is merge 1 π
movieList: (prevMovie, nextMovieList) => /* ... */,
// πππ
},
},
make: /* ... */,
});
makeUpdateMovie.js
import { defineFetch } from 'resift';
const makeUpdateMovie = defineFetch({
displayName: 'Update Movie',
share: {
namespace: 'movieItem',
merge: {
// this is merge 2 π
movieList: (prevMovie, nextMovieList) => /* ... */,
// πππ
},
},
});
So in this situation, there are 2 namespaces: movieList
and movieItem
and there are 3 fetch factories: getMovies
, makeGetMovie
, and makeUpdateMovie
.
The ambiguity comes from the following question: when a SUCCESS occurs the movieList
namespace (i.e. the getMovies
fetch factory), which merge ("merge 1" or "merge 2") is ran?
The answer to this question is actually which ever merge was associated with a fetch factory that was dispatch last β a convoluted and weird answer.
Instead of having the merges in the fetch factories, I propose we hoist them up to the <ResiftProvider>
using a merges
property. The convention would be to create a new file or folder called merges
that will handle doing the merging from all namespaces to other namespaces.
index.js
import React from 'react';
import { render } from 'react-dom';
import { ResiftProvider } from 'resift';
import App from './App';
import dataService from './dataService';
import merges from './merges';
const container = document.querySelector('#root');
render(
<ResiftProvider dataService={dataService} merges={merges}>
<App />
</ResiftProvider>,
container
);
merges.js
const merges = {
incomingNamespace: {
destinationNamespace: /* ... mergeFunction ... */,
},
movieList: {
movieItem: (previousMovieItem, nextMovieList) => {
// return a new movieItem using the `nextMovieList`
}
},
};
export default merges;
The proposal is simple: instead of having the merges on the fetch factory, hoist them to the provider.
This will actually make the code much more simple. We can remove a lot of confusing logic in the reducers.
ReSift/src/dataServiceReducer/sharedReducer.js
Lines 23 to 31 in d0b6a1f
πthis code snippet is creating a lookup table of merge objects on FETCH
so that it can correctly know how to merge in an incoming SUCCESS
. If that sounds convoluted, it's because it is π
.
If we hoist the merges to the provider level then we can deprecate this code and make things a bit more straightforward.
Problem: It's ambiguous which merge function will run when there is more than one merge function for the same source-target pairing of namespaces.
Solution/Proposal: Hoist the merges
definition to the provider.
API Changes: share.merge
will be deprecated (or possibly removed if possible) in favor of adding a merges
prop to the <ResiftProvider>
component.
This issue is to track to two things:
The bundle size in ReSift is not great, I think it's acceptable but we could do better.
According to bundlephobia, ReSift is 13.9kB minified and zipped.
However, we currently require two peer deps, redux 2.6kB and react-redux 5.5kB brining the total footprint of ReSift to around ~22kB (given that you're not already using react + react-redux).
Here is the result of running webpack-bundle-analyzer (without redux or react-redux):
My biggest takeaways:
window.fetch
because ReSift needs the ability to cancel the request.useStatus
on our side is relatively large and can be reduced in size easilyIf we do those, we can shave off 7-8kB off the bundle taking the ReSift core bundle (without Redux) to be around 6.4kB and with Redux, around 14.5kB.
I think these items are a good first iteration goal of reducing bundle size. There will be more investigation of removing redux and react-redux for the purposes of concurrent mode in #32 but for now, I don't think removing redux is worth focusing on for a stable release.
we currently bundle using webpack with the library option of UMD. My current understand is that this way of bundling does not support tree-shaking (though in reality, you'll probably end up using all of ReSift so tree shaking is somewhat irrelevant).
I know other libraries like react-router use rollup. I'd like to switch as well. In general, we just need a bundling audit.
We've had a reoccurring issue with double spinners where there are two loading spinners are on the page for similar requests.
I think we can solve this issue using subscription mechanisms + context.
We can create a component (for lack of a better name) <ContextLoader>
that adds a context provider over its children. If we use another <ContextLoader>
inside of this loader, if the parent loader is loading, then it will prevent any of the children loaders from revealing.
I don't have an implementation ready at this point but my current thought is that the context shape will be some sort of set. Each time a nested provider is added to the tree, it will add something that allows the child component to pull its state.
When a loader tries to render a view, it will first check the state of the parents.
Hey all, I have a tentative for roadmap for ReSift!
This list isn't set in stone and there may be other things added later but this should communicate the prioritizes and the order of things to be released.
I think it makes a lot of sense to adopt an experimental channel like react has adopted to align with the features they've released in that channel.
Supporting Concurrent Mode and Suspense #32 is definitely a high priority, however, realistically, we can't have a stable release until React has a stable release. There will be a lot of work in the stable/latest channel alongside the experimental channel.
Here is a very tentative roadmap:
0.1.1
type
or similar option to useStatus
e.g. useStatus(getPerson, { type: 'self' })
.self
, namespace
, and all
. This will deprecate isolatedStatus
(issue coming soon)0.2.0
0.2.1
useCancel
)0.3.0
useTransition
and useDeferred
value internally in dispatch
and useData
(respectively) (this will probably require removing redux)Stability is something we care about deeply with this library so it's important for us to have a plan/todo list that includes everything that's required in order to finish the initial release of this library.
The good news is that the code for this library is done (since we use it in production at Sift), however there is still a list of things to complete for our initial release.
Here's that list:
react-hooks/exhaustive-deps
Please let me know if there is anything else missing for this list or if you have any insight in any of the tasks above.
Thanks everyone!
@pearlzhuzeng @ricokahler
const data = await http(requestParams, httpOptions);
I have a use case where I need to add additional headers to a specific fetch. I'm going to write a PR that includes this. Below is an example of a fetch using this.
I'm going to locally make something work and post my PR.
http({
method: "GET",
route: "...",
headers: { "foo": "bar"}
});
export default defineFetch({
displayName: 'Get People from listId',
make: (listId: string) => ({
key: [listId],
request: () => ({ http }) =>
return http({
method: 'GET',
route: `/list/${listId}/members/`,
}),
}),
});
From all the times I've used Resift, I have never had to deviate from what I passed into the make value, to then into the key value.
To keep things more simple for Resift, I purpose default the key value to be the arguments passed into the make function. If there needs to be an override, be explicit and declare the key value to be something unique.
Hey Rico, letting you know about this issue and in no way do I feel you are responsible for fixing this. In the short term, Sift will disable hermes because we do not debug from android devices very often.
When resift library is import'ed on an android react native application with hermes enabled, the application will error out saying "React Native Hermes: undefined is not a function js engine: hermes"
Seems like Hermes does not want to render the build file from resift. And due to the entire resift library being on one file, it is rather challenging to quickly see where in the build index.js file is there not a function defined.
Hermes is an open-source JavaScript engine optimized for running React Native apps on Android. For many apps, enabling Hermes will result in improved start-up time, decreased memory usage, and smaller app size. At this time Hermes is an opt-in React Native feature, and this guide explains how to enable it.
This issue exists to track progress on the ReSift tutorial created by @pearlzhuzeng .
Her docs were merged into the v0.1.0 branch however they are not currently in the sidebar. We'll use this issue to track progress.
This issue is here to track how we'll do API documentation.
I think we should try experimenting with semi-automatic API documentation using typedoc since we already have typings in our repo but I'm also open to ideas on how to handle this.
Typedoc doesn't work for .d.ts
files in my experiments so I opted to write some parsers. Check it out in #13 .
This issue is here to experiment and try different ways of doing API documentation.
Jules also started some manual API docs that we can migrate over to the autogenerated versions see #1
Thoughts are appreciated!
The linter and the linting rules were not carried over from resift-internal in order to save time, however that should definitely be done.
This issue exists to determine:
react-hooks/exhaustive-deps
though)This issue exist to track the progress on this.
This issue is here to track the ReSift docs. Here is a tentative outline for the docs.
Feel free to suggest any additions or take ownership of any particular doc.
Introduction (required docs):
Main concepts (required docs):
Loader
componentuseError
hook, the ERROR
status, try catching dispatching, and the onError
callback in the data service parametersExamples:
These would be great as code-sandboxes.
Tutorial (nice-to-have):
API docs (required but should be auto generated):
The lists feature showed us that we have a lot of work to do in order to get error handling right in ReSift. The current philosophy is that we should treat errors as rare events that cause the data to "stop flowing" and potentially crash the page.
Lists proved to us that that is definitely not the case. Errors should be accepted as a common use case and we should structure ReSift to handle errors better.
In the current state of ReSift, the way to handle errors is to try
catch
them in either the fetch factory request body OR where we called dispatch
e.g.
in the fetch factory request body:
const makePersonFetch = defineFetch({
displayName: 'Get Person',
make: id => ({
key: [id],
request: () => async ({ http }) => {
try {
await http({/* ... */});
} catch (e) {
// do something with e
}
},
});
where we called dispatch
const handleThing = () => {
try {
await dispatch(/* ... */);
} catch (e) {
// do something with e
}
};
Also, once an error happens, useFetch
will return null
instead of the data. Also there is no way to get the error body/data from the response once an error occurs.
Anyway, we have a lot of problems to solve so here is yet another proposal:
useError
useError
is a custom hook that will return any thrown error in a fetch factory's request function.
const MyComponent({ id }) {
// ...
const personFetch = makePersonFetch({ id });
const error = useError(personFetch);
return <div>
{error && <ErrorBanner>{error.message}</ErrorBanner>}
{/* ... */}
</div>;
}
useError
also aligns with #21
When originally designing the API for useFetch
, I thought there would be seldom cases for when you would only need the status of a fetch without needing the data.
However, with the addition of proposal #20 useGuard
, it would become a much more common case where you would only need status
.
So why not split up useFetch
into two separate hooks? It seems to be what the community is doing (looking at the new hooks API from react-redux and react-router are doing).
How does adding useData
and useStatus
sound?
Hey,
I'm intrigued by this library, but I'm not yet clear on whether it would fit our use case. Currently, coordinating all the different fetches and their state is a big headache, which ReSift seems to have a good solution for.
I'm curious however how I'd incorporate stuff like optimistic updates (immediately setting state, then potentially rolling back if api says nay) and local state changes which do not involve communicating with a server in general.
ReSift shares are essential features in order to keep consistency across our apps. However, I've noticed a problem with the current ReSift shares model and I'd like to propose a non-breaking solution for this.
Let's say you have a fetch that represents a collection of things.
moviesFetch.js
const { defineFetch } from 'resift';
const makeMoviesFetch = defineFetch({
displayName: 'Get Movies',
make: () => ({
key: [],
request: () => ({ http }) => http({
method: 'GET',
route: '/movies',
}),
}),
});
const moviesFetch = makeMoviesFetch();
export default moviesFetch;
And you also have another fetch that represent getting one of those things from that collection
makeMovieFetch.js
import { defineFetch } from 'resift';
const makeMovieFetch = defineFetch({
displayName: 'Get Movie',
make: id => ({
key: [],
request: () => ({ http }) => http({
method: 'GET',
route: `/movies/${id}`,
}),
}),
});
export default makeMovieFetch;
And then you have one more fetch that updates a movie:
makeUpdateMovieFetch.js
import { defineFetch } from 'resift';
const makeUpdateMovieFetch = defineFetch({
displayName: 'Update Movie',
make: id => ({
key: [id],
request: updatedMovie => ({ http }) => http({
method: 'PUT',
route: `/movies/${id}`,
data: updatedMovie,
}),
}),
});
The issue happens when you try to share all of these fetches. Each fetch is related because they come from the same collection, however, sharing them may lead to undesired results when pulling data.
When fetches are shared, they share the same store and this means that the moviesFetch
(i.e. the list) will share the same state as the getting a single movie makeMovieFetch
(i.e. an item from the list).
If you already have these fetches in use and you want to share the fetches to ensure consistency, then sharing the fetch would require changing the component's use of useFetch
because the shape of the data coming back is different between moviesFetch
and makeMovieFetch
.
e.g.
before the share
function Movie({ id }) {
const movieFetch = makeMovieFetch(id);
const [data] = useFetch(movieFetch);
console.log(data); // { title: 'Paddington 2 } (just one movie)
}
after the share with movies
function Movie({ id }) {
const movieFetch = makeMovieFetch(id);
const [data] = useFetch(movieFetch);
console.log(data); // [{title: ...}, {title: ...}]; now it's a collection of movies??
}
I think the root cause of this problem is forcing shares to have the same shape.
The solution to this problem is allow fetches that have different shapes to have separate state but allow fetches to watch for changes from other namespaces.
The API I'm proposing will look like this:
moviesFetch.js
import { defineFetch } from 'resift';
const makeMoviesFetch = defineFetch({
displayName: 'Get Movies',
share: {
namespace: 'movieList',
merge: {
// movieList: (previousMovies, nextMovies) => nextMovies, // this is the default behavior
movieItem: (previousMovies, updatedMovie) => {
const index = previousMovies.find(movie => movie.id === updatedMovie.id);
const nextMovies = [
...previousMovies.slice(0, index),
updatedMovie,
...previousMovies.slice(index + 1, previousMovies.length),
];
},
},
},
make: () => ({
key: [],
request: () => ({ http }) => http({
method: 'GET',
route: '/movies',
}),
})
});
makeUpdateMovieFetch.js
import { defineFetch } from 'resift';
const makeUpdateMovieFetch = defineFetch({
displayName: 'Update Movie';
share: {
namespace: 'movieItem',
merge: {
movieList: (previousMovie, movieList) =>
movieList.find(movie => movie.id === previousMovie.id),
},
},
});
The idea here is that share.merge
can now be an object and react to changes in other namespace by using their namespace key in merge.
If the default behavior of the share is to be overridden then its own namespace key should be used as the merge key.
Let me know what y'all think! I think this could be very helpful for the Sift Lists feature on both web and mobile.
@pearlzhuzeng had a great idea for creating a page that shows a bunch of code examples for ReSift. She suggested we create a page similar to react-spring's example page so we can show off our cool demos and allow the community to add their own.
A while ago @JulesPatry had some ideas regarding changing the naming convention of fetches which have a lot of appeal.
He suggested that we drop the -fetch post-fix in favor of always having get
, update
, create
, and delete
in the name of the fetch.
current convention:
const makePersonFetch = defineFetch({/* ... */});
const personFetch = makePersonFetch(id);
const [person] = useFetch(personFetch);
const makeUpdatePersonFetch = defineFetch({/* ... */});
const updatePersonFetch = makeUpdatePersonFetch(id);
const [person] = useFetch(updatePersonFetch);
proposed convention (credit to jules):
const makeGetPerson = defineFetch({/* ... */});
const getPerson = makeGetPerson(id);
const [person] = useFetch(getPerson);
useEffect(() => {
dispatch(getPerson());
}, [dispatch, getPerson]);
const makeUpdatePerson = defineFetch({/* ... */});
const updatePerson = makeUpdatePerson(id);
const [person] = useFetch(updatePerson);
useEffect(() => {
dispatch(updatePerson());
}, [dispatch, updatePerson]);
Jules's convention works because of the addition of the CRUD verb. We know that a fetch is a fetch if it has a CRUD verb so we can get rid of the fetch
post-fix.
The React team has finally released docs for suspense for data fetching and concurrent mode π. They've clarified a bit of how it works and how its intended to be used, and my first impression is that it can actually fit within our current APIs nicely without any big breaking changes.
Reading their docs is a prereq to this issue. and so is watching this talk from the Relay team.
They say the correct way to use suspense is to "render as you fetch", meaning that you reveal parts of the API that make sense to reveal if you have enough data to reveal it. Here is a direct quote from their new docs:
Although itβs technically doable, Suspense is not currently intended as a way to start fetching data when a component renders. Rather, it lets components express that theyβre βwaitingβ for data that is already being fetched.
which sums up the idea of "render as you fetch".
What does this mean for ReSift?
Well, fortunately, not that much. Many simple fetching libraries rely on "fetch-on-render" techniques because the state of the fetches inflight live in the component. Fortunately for ReSift, the state of our fetches lives in global state, and we've already discovered that we need to split up fetching (e.g. dispatch(getPerson())
) from getting the data (e.g. useData(getPerson)
).
This means that we can advise users to initiate fetches for components at some higher component level to abide by the "already being fetched"/"fetch-as-you-render" philosophy. This is possible because our useDispatch
and useData
APIs are separate.
Here's how I think it should work (disclaimer: i have not tested to see if this will work):
useData
should suspend by default (i.e. throw a promise) and we should add a configuration object with the flag noSuspend
(or maybe allowNull
or similar) to allow the hook to return null
instead of causing suspense (e.g. useData(getPerson, { noSuspend: true })
).dispatch
work with startTransition
of useTransition
I still have to test whether this approach makes sense but there's my initial thoughts on how that should work.
Suspense + concurrent mode should stay in an experimental as long as React has them in an experimental channel. See #37 for more details.
TL;DR this is a proposal enables us to write code like this:
import React from 'react';
import thingFetch from './thingFetch';
import { useData, withLoaderBoundary } from 'resift';
function Thing() {
// thing will always be fetched
const [thing] = useData(thingFetch, dispatch => {
dispatch(thingFetch());
});
return <div>{thing.stuff}</div>
}
export default withLoaderBoundary(Thing);
ReSift will now handle dispatching fetches for you and (unlike useFetch
) the data useData
returns will always be defined and fetched like magic.
Not TL;DR
So I was reading Dan Abramov's article on "algebraic effects" and in that article he linked a tweet/gist from Sebastian MarkbΓ₯ge.
This gist implements a way to get values from promises without using .then()
and without using async
/await
which is pretty fascinating/game changing.
Check it out:
usage:
function getUserName(id) {
var user = JSON.parse(fetchTextSync('/users/' + id));
return user.name;
}
function getGreeting(name) {
if (name === 'Seb') {
return 'Hey';
}
return fetchTextSync('/greeting');
}
function getMessage() {
let name = getUserName(123);
return getGreeting(name) + ', ' + name + '!';
}
runPureTask(getMessage).then(message => console.log(message))
implementation:
let cache = new Map();
let pending = new Map();
function fetchTextSync(url) {
if (cache.has(url)) {
return cache.get(url);
}
if (pending.has(url)) {
throw pending.get(url);
}
let promise = fetch(url).then(
response => response.text()
).then(
text => {
pending.delete(url);
cache.set(url, text);
}
);
pending.set(url, promise);
throw promise;
}
async function runPureTask(task) {
for (;;) {
try {
return task();
} catch (x) {
if (x instanceof Promise) {
await x;
} else {
throw x;
}
}
}
}
And then someone replied to this tweet implementing the same concept but using React with error boundaries:
The code example above is getting values from promises "synchronously" via hacking the throw
statement. When a component encounters these pseudo promises, it throws stopping the current execution. However, when the promise actually resolves, there's a loop that will re-try the execution. Since there is data, the flow will execute without throwing.
Since the React components are pure/idempotent, we can have them partially re-execute without the worry of side-effects.
When you add React's error boundaries with componentDidCatch
, we can implement this idea of "loader boundaries".
Loader boundaries are components that will prevent rendering of components with fetches until the values from those fetches become defined. What these boundaries will do is "catch" thrown fetches and prevent rendering until those fetches are finished.
This enables us to write hooks that will always assert that data is there and furthermore we can make these hooks calls dispatch fetches for data if there is none.
The goal of this proposal is to have this hook useData
(tentative name) that will:
function Person({ personId }) {
const personFetch = makePersonFetch(personId);
// person will always be defined
// this hook will also dispatch a request if there isn't an inflight one yet
const [person] = useData(personFetch, dispatch => {
dispatch(personFetch())
});
return <div>{person.name}</div>;
}
function App() {
<LoaderBoundary fallback={<CircularProgress />}>
<Person id="person123" />
</LoaderBoundary>
}
No more dispatch
ing, no more handling null
. Just use the data.
Loader
(from skipper) allows us to wrap part of a component with a spinner but this pattern would force us to split up our components more because one component would stop renderinguseData
calls if the fetch status becomes unknown suddenly. This may cause state to be reset too often.Let me know what you think!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.