nanostores/query

Suggestion: Add `isPending` State

martinmckenna opened this issue · 6 comments

both fetcher and mutator stores return a loading, data and error state, but the issue here is that if your app needs to show some kind of empty state, you will get a flash of empty state before the loading gets set to true

That means in a React component like this for example:

const { data, loading, error } = useStore($someQuery)

if(loading) {
   return <span>loading</span>
}

if(error) {
  return <span>{error}</span>
}

if(!data) {
   return <span>no data here!<span />
}

return data.map(eachThing => ....)

the "no data here" condition flashes quickly before the loading state appears

React query solves this by exposing an additional property called isPending:

For most queries, it's usually sufficient to check for the isPending state, then the isError state, then finally, assume that the data is available and render the successful state:

https://tanstack.com/query/latest/docs/framework/react/guides/queries#query-basics

If this library returned this additional property before the request has been fired off, the consuming UI could use it to show a loading state without getting a flash before the loading state is set to true

@martinmckenna The reason you see flashes is pretty simple: you have an incorrect order of returns. See those 3 keys are completely independent, it's not a state machine that can either have loading or data or error. For example, you can have stale cache set in data and when it's time to revalidate it, loading will toggle, even though data will still be there.
As I mention in README, the best UX is produced by this snippet:

const Post = () => {
  const { data, loading } = useStore($currentPost);

  if (data) return <div>{data.content}</div>;
  if (loading) return <>Loading...</>;
  
  return <>Error!</>;
};

This way if any data is present, it will be there without any loaders. If you want to be a bit more on the safe side with checks, you can write data !== undefined, that would be a bit more correct.


As for zero data state, I feel like it should be solved a bit differently. Again, it boils down to how API is designed, but when I was drafting the API surface of this library, I analyzed literally all APIs I worked with over the last 10 years, and it turned out none of them had undefined as the result of execution.

So, for a GET /post/1, if there's not post, it would be error 404 in 98% of the cases, but in the rest 2% of the cases it would be {status: 404, message: "notFound"} or something like this—so data is never undefined (at the very least—null?). For GET /post an empty state would be an empty list.

So, in this case, as I think, you should have some ways to distinguish between zero- and non-zero states with current API. Just treat data === undefined as isPending.

I'll close it for now, lmk if there's something unclear or still worth exploring.

yeah I mean we definitely implemented some computed values from the base loading and data states. For example, a lot of our components look like this:

const {loading, error, data} = useStore()

const noData = typeof data === "undefined"
const isFirstLoad = loading && noData

if(isFirstLoad || noData) {
   return <span>loading....</span>
}

if(error) {
   return <span>error</span>
}

return <Blah />

This is to solve 2 issues.

  1. We don't want to re-show the loading state if the user is taking an action that's going to invalidate the data because we choose to show the old data until the new data becomes available
  2. We don't want to flash a no data state before the loading state kicks in. We want the page to initialize with the loading UI

So for these reasons, having a state that implies the request hasn't fired off yet would be a nice-to-have and prevent us from writing a bunch of extra code in the consuming code.

const Post = () => {
  const { data, loading } = useStore($currentPost);

  if (data) return <div>{data.content}</div>;
  if (loading) return <>Loading...</>;
  
  return <>Error!</>;
};

this will show <>Error</> for a split second before the loading gets set to true

this will show <>Error</> for a split second before the loading gets set to true

😨
tis not gud

UPD: can confirm. Welp, we in fact found the issue worth solving in this thread, cause this shit is not ok, why does it freaking spend an entire tick to set loading to true? it really shouldn't

yep. I think that's why react query has separate isLoading and isPending states, and recommends you only really need to do checking on isPending since it initializes as true