-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use cases, features and considerations #3
Comments
Here's the comparison of usages with hooks in three libs. They are all similar and slightly different. immediate fetchreact-asyncconst { data, error, isPending } = useFetch(`https://swapi.co/api/people/${id}/`); react-async-hook// define this outside of render
const fetchFunc = async id => (await fetch(`https://swapi.co/api/people/${id}/`)).json();
const { result, error, loading } = useAsync(fetchFunc, [id]); react-hooks-asyncconst { result, error, pending } = useFetch(`https://swapi.co/api/people/${id}/`); fetch in callbackreact-async// define this outside of render
const fetchFunc = async id => (await fetch(`https://swapi.co/api/people/${id}/`)).json();
const { data, error, isPending, run } = useAsync({ deferFn: fetchFunc });
return <button onClick={() => run(1)}>click<button>; react-async-hook// define this outside of render
const fetchFunc = async id => (await fetch(`https://swapi.co/api/people/${id}/`)).json();
const { result, error, loading, execute } = useAsyncCallback(fetchFunc);
return <button onClick={() => execute(1)}>click<button>; react-hooks-async// define this outside of render
const fetchFunc = async ({ signal }, id) => (await fetch(`https://swapi.co/api/people/${id}/`, { signal })).json();
const { result, error, pending, start } = useAsyncTask(fetchFunc);
return <button onClick={() => start(1)}>click<button>; custom fetch hook with abortabilityreact-asyncconst useFetchHero = () => useAsync({
deferFn: useCallback(async ([id], _props, { signal }) => {
return (await fetch(`https://swapi.co/api/people/${id}/`, { signal })).json();
}, []),
}); react-async-hookconst useFetchHero = (id) => useAsyncAbortable(async (signal, id) => {
return (await fetch(`https://swapi.co/api/people/${id}/`, { signal })).json();
}, [id]);
// It does not support the pattern with callback? react-hooks-asyncconst useFetchHero = () => useAsyncTask(useCallback(async ({ signal }, id) => {
return (await fetch(`https://swapi.co/api/people/${id}/`, { signal })).json();
}, [])); |
Good to have this as a reference, thanks. I've been thinking to remove the distinction between promiseFn and deferFn and introduce a different way to handle scheduling/triggering, a bit like react-hooks-async. |
thanks @dai-shi that's a great comparison For the future it would be great to discuss what we have found great/bad in our own design decisions. For me I like how my lib api better (particularly that the async fn and the dependency array can typecheck, or freedom given by passing a no-arg async function). Also think the ESLint plugin can be configured to check the dependencies (need to verify that, but there's a conf for custom hooks). Not totally fan of how I handle cancel signal (I'd rather not have a custom hook for this usecase). React-async seems quite similar to me but less convenient and easy to typecheck. For react-hooks-async I'm not totally sure to understand why the task abstraction is needed. Maybe you can explain better @dai-shi what's the advantages of using your lib that I may not be able to see? |
@slorber Could you elaborate on what you think is better about I'm also not really keen on the task abstraction in The thing I've come to reconsider with React Async is the |
I had two motivations in developing react-hooks-async. AbortabilityA general interface for abortable promises is ComposabilityAs I thought developing useAsync/useFetch naively is somewhat trivial, I wanted something unique in my lib. That is to replace redux-saga in some use cases. Of course, it doesn't replace it at all, but I mean some small use cases could be done much more simply without it. I also saw a requesting comment like that in Reddit. So, useAsyncCombine* hooks are crucial in react-hooks-async. (My experiment without composability is If we have the async task abstraction, we can combine them as we like. There could be more combining hooks than what I have now. Honestly, I'm not sure if this useEffect chaining (hope you get what it means?) is a good practice. There might be a better abstraction with Suspense. Apart from the technical challenge in react-hooks-async, the use cases I want to cover in this Async Library project is something like redux-saga (I'd admit I have zero experience with it though). One of the questions I have is, given the domain of "async" is larger than that of "data fetching", what would be async use cases other than data fetching? Or is it equal? |
What I like in my API is:
What I don't like is to have a custom hook for handling abortability, and not totally fan of my state shape or returned api. This is not bad but probably could be better. thanks @dai-shi About the composability without loosing the cancellability, couldn't we allow users to plug this from the outside? I suspect there's a common misconception about abort controller, for example most people think code like this is safe:
If the cancel signal is triggered during the delay, people probably assume the async process will be canceled while it actually won't at all. I think your library is interesting as it handle cancellation without letting the user falling in such trap (I guess?), but I wonder if it couldn't be simpler and if the task abstraction is needed for that (maybe something like https://www.npmjs.com/package/p-cancelable or similar) Also, like @ghengeveld seems to think, couldn't we implement this task abstraction as a plugin, and keep the core more simple (just handling a single async fn)? Not totally sure how you compare your project to Redux saga. I know sagas quite well and actually helped design and spread the library, but for me it's really 2 separate things.
I use this regularly on RN apps to read about async things not related to fetch. For example, checking if current app is granted permission to access user camera is an async function. This kind of lib makes sense in a lot of cases that are totally unrelated to fetching data for me. |
I think I'm doing this... It allows a single async function without using useAsyncCombine*, which are plugin hooks.
I would like to confirm if we are on the same stage. The fundamental difference I see for composability is only
So, only if we added I might be misunderstanding something, so feel free to ask/correct.
I wouldn't disagree. Let me phrase it differently: In some cases, people overuse redux-saga or redux-observable for their use cases which only require cancellation (so, no saga power). Such use cases could be covered by a simpler hook-based solution.
Oh, I see. Thanks for the example. |
I think we should at least handle promise cancellation in order to prevent race conditions. AbortController is optional, as not every browser supports it. Nevertheless I think this should be built-in because otherwise it will be forgotten by most developers, which is a shame. I would be okay with leveraging the AbortController API to handle cancellation, as an alternative to checking if the promise reference is still actual or checking against a counter. In that case we have to polyfill the AbortController to handle our usage. Working with Promises instead of fetch has always been the basis of React Async, because it allows for composability simply by composing promises (e.g. with Promise.all). It's not as flexible as a task-based interface, but it saves us from having to build and maintain complex task scheduling logic. In practice I think not many people will need it, especially when you use Suspense to model the async state tree (and thus interdependencies between async operations). @dai-shi perhaps you can elaborate on the use cases you can cover with the task based approach? I always prefer to speak in terms of real-life use cases when discussing technical abstractions. @slorber I think those are very valid points which we have to keep in mind when designing a consolidated API. It should be very hard to shoot yourself in the foot, and typechecking is a good way to ensure that. I like your point about the eslint rule, it would be really nice if we could leverage that. On a final note: one of my goals is for the async process to become inspectable and controllable through DevTools (as a Chrome extension, eventually). I think this will give developers much better tooling to deal with HTTP requests and other async operations as compared to the Network tab. For example we can offer pausing, delaying and replaying for each individual operation, rather than throttling the entire network or having to refresh the page to replay a request. I consider this part of the core library, so we can potentially release |
That's very neat!
Yeah, that should be more JS centric.
Agreed.
In the case of Suspense, we need to execute async func in render, correct?
Only the realistic use case I have is the typeahead example. The other use case I see from the user feedback is something like this:
We could obviously implement this as a single async function. |
Quoting the previous comment:
I think I'm proposing a new paradigm in the async + hooks world. This may or may not be the mainstream. So, your comment is totally valid and maybe true. By the way, this is probably something related with @dagda1 's comment in slorber/react-async-hook#15 (comment).
I don't disagree with him. Execute in useEffect and execute in callback are different. |
Hello, I have a few more points to consider: Type inference (Typescript)This is a must in my opinion. When using javascript it is a pain to use jsdoc to declare types. The problem is that we can't put everything into an Object or Map because we lose the types doing this. I suggest an API that returns an object (or class instance) which is to be exported and used by the user, much like redux actions. For example: import { createResource } from 'async-library'
export const userInfoResource = createResource(...)
export const userEventsResource = createResource(...) Performance (Tree shaking, extensibility and bundle split)JS code is costly for initial render and performance, so we should ship as little code as possible. Two ways of doing this:
I suggest we do both. The example above is a good way of doing this, because (unlike redux which usually declares the whole store at the index) the code will only be bundled into the chunks that need it. We can also declare middleware for things like caching, pooling and retrying. We can also use helpers to build Separation of API'sI see most comments are thinking of a hook-like solution. I've working with svelte the past few months and I think it's solutions are very elegant. The way stores work (check the docs and this example) are very simple and effective.
Only the Framework API here should use the hooks logic (and not for all frameworks) ExampleI wrote a gist as an example for this, it has the API declaration (like any .d.ts) and an example use case MissingThe only thing I think I'm missing is the subscription (websocket), did not think about that yet |
I also like the way rest-hooks work. The only thing I disagree is that the Resource class comes with everything (get, update, delete, lists, ...) already bundled, so it loses on performance because of that (I known 9kb gzipped might look lightweight, but consider that a page built with svelte may be only 3kb gzipped). |
I came across resourcerer which has some pretty interesting ideas. Particularly critical vs. noncritical resources, the way it handles dependencies between requests and built-in support for prefetching. Not things we want to have in the core but good use cases to consider. |
Also, React Suspense is going into preview right now. I hope to play around with it in the near future, maybe that gives us some additional ideas: |
Are you at React Conf? Keep us in the loop on relevant updates 👍 |
Okay so I spent some hours building an initial version of the core state machine: https://codesandbox.io/s/async-library-state-machine-084sw (use the Tests tab) Let me know what y'all think. So far it does:
It doesn't actually deal with async functions yet. This is just pure synchronous vanilla JS. |
Nah, I'd love to be, but I just was at IJS in Munich the last few days. Plugged React-Async and Async-Library in my Talk though ;) But my twitter is full of ReactConf right now, I guess it's the next best thing :D |
I also spent some hours experimenting on the idea I described above for the core API. Check out the repo:
It has just a few tests, so it may be hard to understand for now.
Usage example: const middleware = {
resolved: jest.fn((options) => options.state),
willLoad: jest.fn(),
willRequest: jest.fn(),
}
const userInfoResource = createResource({ fn: getUser }, [() => middleware])
userInfoResource.subscribe(state =>{
console.log('got a new state: ', state)
})
userInfoResource.run({ id: 1 }) // run There is also an example middleware for caching (I will translate to js for those not familiar with ts): export const inMemoryCache = () => (/* IResource param */) => {
const cache = {}
return {
cache: (args, next) => {
const value = cache[args.options.hash]
if (value === undefined) return next(args)
return next({
...args,
state: value,
})
},
// save value to cache
resolved ({ options: { hash, state } }) => {
cache[hash] = state.data
},
}
} |
Nice. Really like how the metadata (and thus the state) can be extended. One question: what do you mean by "race conditions"? Since JS is single threaded, I'm probably misunderstanding something |
I like both those approaches (and certainly am thinking of a few ideas of my own), but while they work great with a given mindset, I'm not 100% sure if they will work for suspense if that should be a major use case for async-library. From what I've seen so far, the idea of suspense seems to be not to restart actions, but to start completely new actions without any context of possible old actions (although cancellation certainly plays a valid role there). Race conditions don't seem to play any role because state is set once at the start, and never at the finish of an asynchronous action. So that seems to make it very different from out mindset at the current time. I'll try to wrap my head around those news idea this weekend, and I'd suggest before we dig deeper, that you also try to play with those ideas (see link above) - this might be a complete gamechanger and we might need to do some serious rethinking to get both mindsets into one model. |
Hi @phryneas , I think it should work with Suspense. We can even implement Suspense for svelte and vue. const useSuspense = (resource) => ({
read() {
// returns a promise that uses the resource api to fetch data
}
}) All we have to do is to add to the resource API so that this is possible |
The way Suspense works isn't much more than throwing a promise, alongside having a way to synchronously return data (from a cache) if it's already loaded. React Async supports this already, simply by setting the I think our main challenge is to offer a compelling API that works fluently with Suspense. We can look at Relay for inspiration. In fact it may be a good idea to just copy it for the most part. We should all get acquainted with the various things Suspense has to offer and concurrent UI patterns. |
Yeah, I have no doubt that whatever we'll think of, in the end it will work with concurrent mode/suspense. But just reading half of the suspense docs yesterday, I have so many new ideas on how this will actually be used in the end and so many patterns I want to consider, that I just would try to get acquainted with those patterns before designing the core that we'll stick with for a long time. |
Please check out my first attempt: https://github.com/dai-shi/react-hooks-fetch |
That's interesting! Why a Proxy? Personally I'm not a fan of the Resource abstraction by the way. I think in terms of data and actions/events, not resources and CRUD. |
To avoid Not sure if I understand the data and action abstraction, but I was hoping resources sound more declarative. |
Resources have a very REST sound to it, which is not good because REST APIs are a lie. Most of them are not RESTful at all, and the ones that are are bloated and unwieldy. GraphQL has a way better abstraction with Queries and Mutations, but essentially it's just RPC. My intention is to support more than just HTTP data fetching (think websockets, workers, native APIs), in which case RPC is a better model than REST. If we want something that's familiar to webdevs, we should go with query/mutation/subscription. |
Until I heard that, I hadn’t thought about REST. That’s understandable. My intention is not to relate with REST. At the same time, I’m worried if query/result mental model fits with Suspense... (Note my comments are all about React. I know this issue is more than that though.) |
I've got another feedback, so trying to rename APIs... (edit) Updated the API, which should be much nicer. import { createFetch, useFetch } from 'react-hooks-fetch';
const fetchFunc = async (userId: string) => (await fetch(`https://reqres.in/api/users/${userId}?delay=3`)).json();
const initialId = '1';
const initialResult = createFetch<
{ data: { first_name: string } },
string
>(fetchFunc, initialId);
const Item: React.FC = () => {
const [id, setId] = useState(initialId);
const result = useFetch(initialResult);
return (
<div>
User ID: <input value={id} onChange={e => setId(e.target.value)} />
<DisplayData id={id} result={result} />
</div>
);
}; (edit2) The above is obsolete. Check out the repo for the latest status. |
Hey all Read the docs about ConcurrentMode etc and asked some stuff to Dan/Sebastian. What I understand from the answers if that if we want to take advantage of the transition feature, and avoid the request waterfall, current approaches/API are probably wrong. https://twitter.com/sebmarkbage/status/1188832461351817216 As far as I understand the answers, deriving a resource (or throwing a promise) at render time will make Suspense work, but not transitions. For transitions to work we need to store a resource outside of React itself. |
I had to look up the request waterfall. It's when you have a fetch in |
This just landed: https://twitter.com/zeithq/status/1188911002626097157 |
I spent a little more time on the core reducer and dispatcher. I pushed the code here: https://github.com/async-library/future/tree/core I followed the Flux pattern to define the dispatcher. What's great about that is it doesn't tie you in to a specific store like Redux does, and it's very lightweight and flexible. Check out the integration test for an idea of what integration with 3rd party libs will look like. Current features:
|
Another interesting approach at data fetching in React: https://github.com/jamesknelson/react-zen |
I've updated the prototype again, adding support for subscriptions and setting data based on old data. I'm not sure yet about the API for subscriptions. Right now it's reusing the existing For reference, the current subscription API looks like this: // Normally `fn` is invoked with params and state.
// For subscriptions it's invoked with fulfill and reject.
const fn = (fulfill, reject) => {
// do your subscription thing, calling fulfill/reject multiple times
}
// `createApp` is just a local name, not a core API.
// This is what would be `useAsync` in a React integration.
const app = createApp({ fn })
// Runs `fn` (once), passing the fulfill and reject callbacks (bound action creators)
app.subscribe() One thing to consider is if we want to support mutations (deferFn) alongside subscriptions. That would be tricky in the current setup because Setup and teardown of the subscription would be done through lifecycle callbacks (onInit / onDestroy), which aren't implemented yet. What's left to do:
|
@ghengeveld , your code is not "type safe" because of the following function: export const compose = (...reducers) => (state, action) =>
reducers.reduce((state, reducer) => reducer(state, action), state)
Anyway, I think we should have a few more things in the core, and think a bit more about the API instead of the implementation.
Sorry for being so critical, just trying to get the best result out of this discussion. Let me know if I was too rough 😬 |
Thanks for pointing that out. My TS experience is very limited, so this isn't immediately apparent to me yet. I see why this is a problem now. Do you have a suggestion for making the reducer (and dispatcher for that matter) composable? One way I can think of is through thunks which basically make reducer composition unnecessary, instead you would probably compose/decorate the action creators. In fact I want to move the special handling of "start" to the
Definitely! This is the time to iterate on it before settling on something for the foreseeable future.
The whole thing is bring-your-own-state by design. So you could put all state in one place, or keep it in local component state. Whatever works for you. However I did consider the need for a way to keep track of ALL instances of Async Library in one central place: the DevTools. The way I plan to do that is have the DevTools listen to all dispatched actions, and keep its own copy of state by running the actions through the reducer another time. It has to hook into the dispatcher if it wants to intercept actions before they are sent to the reducer (e.g. to delay or rerun it).
Yes, I designed this to be extendable by whoever develops the integration (so not the end user, typically). For example AbortController might not make sense for a Node.js or React Native integration (not sure though). I've not designed the API for it yet. I'm also not sure if this is the best way to do it.
Not at all! Please be critical of my work, I don't have all the answers and I probably made some silly choices in some places. |
Another one just landed: https://twitter.com/tannerlinsley/status/1191472293643350021?s=20 Interesting detail is that it uses a key to determine same-ness: https://twitter.com/tannerlinsley/status/1191591012981805059?s=20 |
Let's collect and decide on the uses cases and features we want to support with Async Library, either directly or through a separate package. They are grouped, but in no particular order. Core would be
@async-library/core
, Integrations would be e.g.@async-library/react-hook
. User provided would be through passing a prop/option. Plugins would be separate packages.⚙️ Core
run(arg1, arg2)
, to support dynamic requests).🧩 Integration
🔌 User provided / plugin
The text was updated successfully, but these errors were encountered: