GithubHelp home page GithubHelp logo

nanostores / query Goto Github PK

View Code? Open in Web Editor NEW
203.0 4.0 9.0 297 KB

⚡️ Powerful data fetching library for Nano Stores. TS/JS. Framework agnostic.

License: MIT License

TypeScript 99.79% Shell 0.21%
async cache fetch graphql nano query stale stale-while-revalidate

query's Introduction

Nano Stores Query

A tiny data fetcher for Nano Stores.

  • Small. 1.8 Kb (minified and gzipped).
  • Familiar DX. If you've used swr or react-query, you'll get the same treatment, but for 10-20% of the size.
  • Built-in cache. stale-while-revalidate caching from HTTP RFC 5861. User rarely sees unnecessary loaders or stale data.
  • Revalidate cache. Automaticallty revalidate on interval, refocus, network recovery. Or just revalidate it manually.
  • Nano Stores first. Finally, fetching logic outside of components. Plays nicely with store events, computed stores, router, and the rest.
  • Transport agnostic. Use GraphQL, REST codegen, plain fetch or anything, that returns Promises (Web Workers, SubtleCrypto, calls to WASM, etc.).
Sponsored by Evil Martians

Install

npm install nanostores @nanostores/query

Usage

See Nano Stores docs about using the store and subscribing to store’s changes in UI frameworks.

Context

First, we define the context. It allows us to share the default fetcher implementation and general settings between all fetcher stores, and allows for simple mocking in tests and stories.

// store/fetcher.ts
import { nanoquery } from '@nanostores/query';

export const [createFetcherStore, createMutatorStore] = nanoquery({
  fetcher: (...keys: (string | number)[]) => fetch(keys.join('')).then((r) => r.json()),
});

Second, we create the fetcher store. createFetcherStore returns the usual atom() from Nano Stores, that is reactively connected to all stores passed as keys. Whenever the $currentPostId updates, $currentPost will call the fetcher once again.

// store/posts.ts
import { createFetcherStore } from './fetcher';

export const $currentPostId = atom('');
export const $currentPost = createFetcherStore<Post>(['/api/post/', $currentPostId]);

Third, just use it in your components. createFetcherStore returns the usual atom() from Nano Stores.

// components/Post.tsx
const Post = () => {
  const { data, loading } = useStore($currentPost);

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

createFetcherStore

export const $currentPost = createFetcherStore<Post>(['/api/post/', $currentPostId]);

It accepts two arguments: key input and fetcher options.

type NoKey = null | undefined | void | false;
type SomeKey = string | number | true;

type KeyInput = SomeKey | Array<SomeKey | ReadableAtom<SomeKey | NoKey> | FetcherStore>;

Under the hood, nanoquery will get the SomeKey values and pass them to your fetcher like this: fetcher(...keyParts). Few things to notice:

  • if any atom value is either NoKey, we never call the fetcher—this is the conditional fetching technique we have;
  • if you had SomeKey and then transitioned to NoKey, store's data will be also unset;
  • you can, in fact, pass another fetcher store as a dependency! It's extremely useful, when you need to create reactive chains of requests that execute one after another, but only when previous one was successful. In this case, if this fetcher store has loaded its data, its key part will be the concatenated key of the store. See this example.
type Options = {
  // The async function that actually returns the data
  fetcher?: (...keyParts: SomeKey[]) => Promise<unknown>;
  // How much time should pass between running fetcher for the exact same key parts
  // default = 4000 (=4 seconds; provide all time in milliseconds)
  dedupeTime?: number;
  // Lifetime for the stale cache. It present stale cache will be shown to a user.
  // Cannot be less than `dedupeTime`.
  // default = Infinity
  cacheLifetime?: number;
  // If we should revalidate the data when the window focuses
  // default = false
  revalidateOnFocus?: boolean;
  // If we should revalidate the data when network connection restores
  // default = false
  revalidateOnReconnect?: boolean;
  // If we should run revalidation on an interval
  // default = 0, no interval
  revalidateInterval?: number;
  // Error handling for specific fetcher store. Will get whatever fetcher function threw
  onError?: (error: any) => void;
  // A function that defines a timeout for automatic invalidation in case of an error
  // default — set to exponential backoff strategy
  onErrorRetry?: OnErrorRetry | null;
}

The same options can be set on the context level where you actually get the createFetcherStore.

createMutatorStore

Mutator basically allows for 2 main things: tell nanoquery what data should be revalidated and optimistically change data. From interface point of view it's essentially a wrapper around your async function with some added functions.

It gets an object with 3 arguments:

  • data is the data you pass to the mutate function;
  • invalidate and revalidate; more on them in section How cache works
  • getCacheUpdater allows you to get current cache value by key and update it with a new value. The key is also revalidated by default.
export const $addComment = createMutatorStore<Comment>(
  async ({ data: comment, revalidate, getCacheUpdater }) => {
    // You can either revalidate the author…
    revalidate(`/api/users/${comment.authorId}`);

    // …or you can optimistically update current cache.
    const [updateCache, post] = getCacheUpdater(`/api/post/${comment.postId}`);
    updateCache({ ...post, comments: [...post.comments, comment] });

    // Even though `fetch` is called after calling `invalidate`, we will only
    // invalidate the keys after `fetch` resolves
    return fetch('…')
  }
);

The usage in component is very simple as well:

const AddCommentForm = () => {
  const { mutate, loading, error } = useStore($addComment);

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        mutate({ postId: "…", text: "…" });
      }}
    >
      <button disabled={loading}>Send comment</button>
      {error && <p>Some error happened!</p>}
    </form>
  );
};

createMutatorStore accepts an optional second argument with settings:

type MutationOptions = {
  // Error handling for specific fetcher store. Will get whatever mutation function threw
  onError?: (error: any) => void;
  // Throttles all subsequent calls to `mutate` function until the first call finishes.
  // default: true
  throttleCalls?: boolean;
}

You can also access the mutator function via $addComment.mutate—the function is the same.

Third returned item

(we didn't come up with a name for it 😅)

nanoquery function returns a third item that gives you a bit more manual control over the behavior of the cache.

// store/fetcher.ts
import { nanoquery } from '@nanostores/query';

export const [,, { invalidateKeys, revalidateKeys, mutateCache }] = nanoquery();

Both invalidateKeys and revalidateKeys accept one argument—the keys—in 3 different forms, that we call key selector. More on them in section How cache works

// Single key
invalidateKeys("/api/whoAmI");
// Array of keys
invalidateKeys(["/api/dashboard", "/api/projects"]);
/**
 * A function that will be called against all keys in cache.
 * Must return `true` if key should be invalidated.
 */
invalidateKeys((key) => key.startsWith("/api/job"));

mutateCache does one thing only: it mutates cache for those keys and refreshes all fetcher stores that have those keys currently.

/**
 * Accepts key in the same form as `invalidateKeys`: single, array and a function.
 */
mutateCache((key) => key === "/api/whoAmI", { title: "I'm Batman!" });

Keep in mind: we're talking about the serialized singular form of keys here. You cannot pass stuff like ['/api', '/v1', $someStore], it needs to be the full key in its string form.

Recipes

How cache works

All of this is based on stale-while-revalidate methodology. The goal is simple:

  1. user visits page 1 that fetches /api/data/1;
  2. user visits page 2 that fetches /api/data/2;
  3. almost immediately user goes back to page 1. Instead of showing a spinner and loading data once again, we fetch it from cache.

So, using this example, let's try to explain different cache-related settings the library has:

  • dedupeTime is the time that user needs to spend on page 2 before going back for the library to trigger fetch function once again.
  • cacheLifetime is the maximum possible time between first visit and second visit to page 1 after which we will stop serving stale cache to user (so they will immediately see a spinner).
  • revalidate forces the dedupeTime for this key to be 0, meaning, the very next time anything can trigger fetch (e.g., refetchOnInterval), it will call fetch function. If you were on the page during revalidation, you'd see cached value during loading.
  • invalidate kills this cache value entirely—it's as if you never were on this page. If you were on the page during invalidation, you'd see a spinner immediately.

So, the best UI, we think, comes from this snippet:

// components/Post.tsx
const Post = () => {
  const { data, loading } = useStore($currentPost);

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

This way you actually embrace the stale-while-revalidate concept and only show spinners when there's no cache, but other than that you always fall back to cached state.

Local state and Pagination

All examples above use module-scoped stores, therefore they can only have a single data point stored. But what if you need, say, a store that fetches data based on component state? Nano Stores do not limit you in any way, you can easily achieve this by creating a store instance limited to a single component:

const createStore = (id: string) => () =>
  createFetcherStore<{ avatarUrl: string }>(`/api/user/${id}`);

const UserAvatar: FC<{ id: string }> = ({ id }) => {
  const [$user] = useState(createStore(id));

  const { data } = useStore($user);
  if (!data) return null;

  return <img src={data.avatarUrl} />;
};

This way you can leverage all nanoquery features, like cache or refetching, but not give up the flexibility of component-level data fetching.

Refetching and manual mutation

We've already walked through all the primitives needed for refetching and mutation, but the interface is rather bizarre with all those string-based keys. Often all we actually want is to refetch current key (say, you have this refresh button in the UI), or mutate current key, right?

For these cases we have 3 additional things on fetcher stores:

  1. fetcherStore.invalidate and fetcherStore.revalidate
  2. fetcherStore.mutate. It's a function that mutates current key for the fetcher. Accepts the new value.
  3. fetcherStore.key. Well, it holds current key in serialized form (as a string).

Typically, those 3 are more than enough to make all look very good.

Lazy fetcher

Sometimes you don't want a store, you just want an async function that's gonna handle the errors and leverage the cache (perform cache lookup, save data in there upon successful execution, etc.).

For that case use fetcherStore.fetch function. It will always resolve with the same data type as store itself (error and data only).

Few gotchas:

  • it will execute against currently set keys (no way to customize them for the call);
  • it will still leverage deduplication;
  • underlying fetcher function cannot resolve or reject with undefined as their value. This will lead to hanging promises.

Dependencies, but not in keys

Let's say, you have a dependency for your fetcher, but you don't want it to be in your fetcher keys. For example, this could be your userId—that would be a hassle to put it everywhere, but you need it, because once you change your user, you don't want to have stale cache from the previous user.

The idea here is to wipe the cache manually. For something as big as a new refresh token you can go and do a simple "wipe everything you find":

onSet($refreshToken, () => invalidateKeys(() => true))

If your store is somehow dependant on other store, but it shouldn't be reflected in the key, you should do the same, but more targetly:

onSet($someOutsideFactor, $specificStore.invalidate)

Error handling

nanoquery, createFetcherStore and createMutationStore all accept an optional setting called onError. Global onError handler is called for all errors thrown from fetcher and mutation calls unless you set a local onError handler for a specific store (then it "overwrites" the global one).

nanoquery and createFetcherStore both accept and argument onErrorRetry. It also cascades down from context to each fetcher and can be rewritten by a fetcher. By default it implements an exponential backoff strategy with an element of randomness, but you can set your own according to OnErrorRetry signature. If you want to disable automatic revalidation for error responses, set this value to null.

This feature is particularly handy for stuff like showing flash notifications for all errors.

onError gets a single argument of whatever the fetch or mutate functions threw.

React Native

React Native is fully supported. For revalidateOnReconnect to work, you need to install @react-native-community/netinfo package. It's optional: if you don't reconnect just won't trigger revalidation. The rest works as usual.

If you use package exports, you can import the library as usual. Otherwise, do this:

import { nanoquery } from "@nanostores/query/react-native";

query's People

Contributors

aramikuto avatar dkzlv avatar hemandev avatar martinmckenna avatar mrfratello avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

query's Issues

Feat: allow any atoms to be passed like keys

Hi, I've been using your library for a while now, and it's doing great, except for a one use case:

I have the following store:

export const $getCurrentUser = createFetcherStore<{ me: UserDTO }, ApiError>(["/users/me", $authStore]);

And for my use case, I expect $getCurrentUser to be run on any $authStore change, but I actually don't want any part of $authStore to be included in the actual URL.
I got it, that you only allow strings or string atoms to simplify the caching mechanism, but it's really nice to have feature, and it's also a thing in the react-query

The other way to solve this, may be by allowing to specify additional dependencies through createFetcherStore second argument, and leave the caching mechanism as it is, but forcing the cache update on every dependency change

export const $getCurrentUser = createFetcherStore<{ me: UserDTO }, ApiError>(["/users/me"], {dependsOn: [$authStore]});

It'll be great to hear your thoughts, maybe you got another take onto this, and if you wish I'll be glad to implement any of the solutions myself and send you a PR

ERR_REQUIRE_ESM with Vitest

Met an error while running tests with Vitest:

Error: require() of ES Module /Users/WebstormProjects/node_modules/.pnpm/[email protected]/node_modules/nanostores/index.js from /Users/WebstormProjects/node_modules/.pnpm/@[email protected][email protected]/node_modules/@nanostores/query/dist/nanoquery.umd.cjs not supported.
Instead change the require of index.js in /Users/WebstormProjects/node_modules/.pnpm/@[email protected][email protected]/node_modules/@nanostores/query/dist/nanoquery.umd.cjs to a dynamic import() which is available in all CommonJS modules.
Serialized Error: {
  “code”: “ERR_REQUIRE_ESM”,
}

Responses with error codes are nevertheless cached until the next dedupe time

Hello,

Using v0.2.8

My fetcher code:

return fetch(url))
        .then((response) => {
            if (response.ok) {
                return response.json();
            }
            throw { status: response.status};
        })

If the server returns an error code, e.g. 500, this url will not be fetched again until the dedupeTime has passed, even though the server might have recovered meanwhile.

I expected that only OK responses get cached. Is there any way to make sure that OK responses are cached, but not errors?

How to use a `FetcherStore` with a `computed()` atom

I'm looking for a little advice on the best way to use one or more fetcher stores from a computed() atom - I'm struggling with what to use to actually subscribe to the fetcher.

Example:

const ids = atom(/* some dymanic thing that returns an array of ids */);

const requestFactory = (id: string | undefined) => {
  const store = createFetcherStore<Response>(['/api/v1/thing/', atom(id)]);
  return store;
}

computed(ids, (ids) =>
    ids.map((id) => {
        const singleStore = requestFactory(id);
        /* How to use `singleStore` here though? */
    }).reduce(/* Something with all these ids against the API call... */);
);

Next.js is supported correclty?

When I try to use a atom I get the following warn:
The result of getSnapshot should be cached to avoid an infinite loop

the data is fetched correctly, is just a warn, also fetcher is called 3-4 times

SSR query in Astro

Is there any way to get nanoquery to fetch during SSR in Astro? This would be quite useful for rendering pages on the server and then dynamically updating them on the client.

Inconsistent Data Loading with Nano Stores Query and Vue.js

Description:

We are experiencing an issue with Nano Stores Query in combination with Vue.js. The data loading is inconsistent, sometimes the data does not load correctly. We use session storage as cache for Nano Stores Query.

Reproduction Steps:

  1. Go to the following example: StackBlitz Example
  2. Log in to see the username displayed.
  3. Reload the page or navigate to the second page (which looks the same).

Expected Behavior:

After reloading the page or navigating to the second page, the user should remain logged in, and the username should be displayed.

Actual Behavior:

Sometimes the user remains logged in and the username is displayed, but other times it does not. Please reload or navigate a few times, it does not always happen - it seems to be a race condition. The console log messages vary based on success or failure.

Console Log Messages:

Success:

image

Failure:

image

Additional Information:

Vue.js version: 3.4
Nano Stores version: 0.10
Browser: Chrome 126

Thank you for looking into this issue. Please let me know if you need any further information or I can help in any way.

Feature request: keep previous data

There are not enough "keep previous data" feature - return the previous key's data until the new data has been loaded.

This feature is well explained in swr: https://swr.vercel.app/docs/advanced/understanding#return-previous-data-for-better-ux

There is an example on @nanostores /query: https://codesandbox.io/p/sandbox/nanostories-query-react-paging-rick-morty-q6ctwm

It shows how the content "jumps" when the user navigates to the next page (when the cache is empty).

Handling mutation errors on per-store level

First mentioned in #27.

The proposed solutions are:

  • adding onError to createMutationStore interface. Maybe, same should be applied to createFetcherStore?
  • (not sure I like this direction) add a conditional to createMutationStore, if mutate should throw or not.

ts-rest integration possibility

Hello,

I'm already incorporating nanostores for outside of components state management.
It's really exciting and usable to have that small size libs. Big thanks for that!

The only thing preventing to use nanostores/query is ability to integrate with modern type-safe API approaches.

I'm aware of dificulties of integration with trpc, but is possible to use integrate nanostores/query with ts-rest?

This is example similar integration of react-query (which I wan to replace with nanostores/query) https://github.com/ts-rest/ts-rest/tree/main/libs/ts-rest/react-query/src/lib

Global fetcher vs transport agnostic

According to the readme, the library is transport agnostic, but then the first thing the docs ask of you is to introduce a global fetcher, which goes against the notion of being agnostic to the transport or lack thereof. It feel that it would make sense to let people decide what fetcher to use for each of the queries rather than introducing a global one. I suppose the idea is that it could be used for testing purposes, but I question the implementation. Could you clarify your intentions with the global fetcher and what you would recommend for mixed transport setups?

how work with mutate

as I understand it, I should just return fetch inside createMutatorStore, but is it possible to make a request to the mutate as in swr there in mutate useSWR('/api/user', fetcher)?

Warning when using a React component in an Astro project

When using nanoquery in a react component in an astro project I get this warning in the console. I'm not familiar enough with react to completely understand this warning so I'm not sure if this is a problem or not.

Warning: The result of getServerSnapshot should be cached to avoid an infinite loop
at NanoQueryExample (http://localhost:4322/src/components/nanoquery.tsx:31:29)

https://stackblitz.com/github/JacobZwang/nanoquery-astro-broken-example
https://github.com/JacobZwang/nanoquery-astro-broken-example

Infinite loop

The problem is reproduced on Vite.

Create base fetcher and store.

I see it in the console:

10297 deduped [my key]
5883 already runs [my key]

Example on StackBlitz

When to use mutations (and not plain functions)

This may be another good topic for documentation, but broadly speaking you want to use mutations in 2 major cases:

  1. you use those loading and error values in the store. Basically, it runs a promise for you and gives some sugar on top of it.
  2. you want to optimistically update cache beforehand (mutations give a good sugar for that as well).

If you don't need either, just go with a good ol' function! Mutations don't really give you any added benefit

Fetcher store always returns `loading: false` despite ongoing Promise

Hello,

I've been experiencing an issue with the fetcher store's loading state in the nanostores/query package. Despite the fetcher returning a Promise that resolves after a delay, the loading state remains false throughout the duration of the Promise.

Here is a simplified code snippet that reproduces the issue:

export const [createFetcherStore, createMutatorStore] = nanoquery({
  fetcher: (...keys: string[]) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({data: 'some data'})
      }, 2000)
    })
  },
})

In this example, I would expect the loading state to be true during the 2-second delay before the Promise resolves. However, I'm consistently observing that loading remains false, despite the fact that the data arrives as expected after the 2 seconds.

I would appreciate any guidance on this issue. If there's any additional information or context that would help diagnose the issue, please let me know and I'll provide it promptly.

Thank you for your time and assistance.

Add Mechanism to Refetch in `createMutatorStore` Without a Loading State

Hi I have a use case like this:

const queryKey = "things"

export const $things = createFetcherStore(queryKey, {
      fetcher: () =>
        getThings().then((response) => response.data),
    })

export const $createThing = createMutatorStore<Payload>(async ({ data, invalidate }) => {
  invalidate(queryKey);
  return createThing(payload);
});

I would really like a way for createMutatorStore to be able to call invalidate to trigger a refetch but without setting $things.loading back to true. I want it to replace the old data with the new data but do it without clearing everything out before because it's fine to show stale data until the new stuff comes back

Either that, or expose a refetch callback on the createFetcherStore return value, so that we can call it from the react component

This seems pretty standard in libraries like react query

Is there a way of dynamically updating `createFetcherStore` inputs?

Hi,

I'm trying to figure out how to update createFetcherStore inputs, when react props change, but can't quite figure it out.

Here is the code:

export const [createFetcherStore, createMutatorStore] = nanoquery({
  fetcher: (...keys: string[]) => fetch(keys.join('')).then((r) => r.json()),
});

const createStore = (tag: string) => () => {
  const store = createFetcherStore<{ content: string }>([
    `https://api.quotable.io/random?tags=`,
    tag,
  ]);
  return store;
};

function Quote({ tag }: { tag: string }) {
  const [$tags] = useState(createStore(tag));
  const { data, loading } = useStore($tags);

  return (
    <p style={{ marginBottom: '20px' }}>
      {loading ? 'loading' : data?.content}
    </p>
  );
}

when tag changes in Quote, createStore won't update and hence the query is not refetched. How would I do this?

Here is an example: https://stackblitz.com/edit/react-ts-bbwme2?file=App.tsx
Notice that the headings change, but the quote is not refetched when changed in the select box.

Feat: tRPC Wrapper

Thanks for the library. I like what you are doing here.
Tanstack Query is a great lib, but I am so happy to see more and more people breaking out of the "React Only" mindset.

tRPC has gotten pretty popular, and I think a client wrapper that offers all the typing in tooling like Astro. build would be pretty awesome. https://trpc.io/docs/client/introduction

Suggestion: Add `isPending` State

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

Extend `loading` indicator

Could we extend loading from boolean to enum maybe? For now, I miss, for example, some initial state, when the request has not yet been made. In current version for this case we have loading: true, but this is not consistent with reality.

Can't install @nanostores/query (npm ERR! code ERESOLVE)

Hi!

I just updated nanostores and @nanostores/query in a project, but got npm errors.
So I tested to install these two packages in a new and empty project and got the same errors...

First I installed nanostores:

➜  npm i nanostores

added 1 package, and audited 2 packages in 2s

That works, but when I install @nanostores/query I get an ERESOLVE error:

➜  npm i @nanostores/query
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: undefined@undefined
npm ERR! Found: [email protected]
npm ERR! node_modules/nanostores
npm ERR!   nanostores@"^0.10.3" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer nanostores@">0.10" from @nanostores/[email protected]
npm ERR! node_modules/@nanostores/query
npm ERR!   @nanostores/query@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

I use node v20.11.1 (npm v10.2.4).
If I try installing an older version of @nanostores/query (npm i @nanostores/[email protected]) everything works fine.

Is this some problem with @nanostores/query or is it my machine?

The counter `retryCount` in the `onErrorRetry` is not incremented

The following code always returns 1 and the retryCount parameter

const [query] = nanoquery({
  fetcher: () => Promise.reject(),
  onErrorRetry: ({ retryCount }) => {
    console.log(`retryCount: ${retryCount}`);
    return retryCount * 1_000;
  },
});

demo: https://codesandbox.io/p/sandbox/nanostories-query-react-replay-count-error-9ywyg2?file=%2Fsrc%2Fstore.ts%3A10%2C23

P.S.: Can you export the default handler onErrorCount? If this is done, then I will be able to set a limit on the number of repetitions of the request and leave the logic of the intervals between requests:

const [query] = nanoquery({
  onErrorRetry: (params) => params.retryCount > 3 ? null : defaultOnErrorRetry(params),
});

Per component instance @nanostores/query without framework's useState()

I followed examples:
https://github.com/nanostores/query#local-state-and-pagination
and #16

I try really hard to move all logic from components to nanostores to have framework-portable code.

I have several instances of data-grid on the same page and according to https://github.com/nanostores/query#local-state-and-pagination created a factory function which creates individual instances of "related atoms and storeFetchers" for data-grid pagination, sorting, synching with URL query-string params.

In #16 @dkzlv writes "the useState hack should rarely be used" and that confused me because if I don't use useState than React gives an infinite-loop on mount.

So the question is:
Why const [store] = useState(myDataGridStoreFactory()); works and const store = myDataGridStoreFactory(); doesn't work?

I don't need to sync with React native component local state as all state is out of components code to nanostores.

Also curious how can MyDatagrid use URL query-string params as "source of truth" to get "pageNumber" and "filters" from URL params?
There could be several data-grids on the page, so nanostore "factory" method will get "pageUrlParamName" as factory initial parameter, but I'm not sure what is the best way to "read/update" URL query-string param from within nanostores without the need to use UI-framework specific code.

Would be really thankful for your help, as documentation doesn't have examples of such use-cases.

My code:

import React, { useState } from "react";
import fetch from "cross-fetch";
import { nanoquery } from "@nanostores/query";
import { atom, computed } from "nanostores";
import { useStore } from "@nanostores/react";

// https://dummyjson.com/docs/users

export { Page };

export const [createFetcherStore, createMutatorStore] = nanoquery({
  fetcher: (...keys: (string | number)[]) =>
    fetch(keys.join("")).then((r) => r.json()),
});

function Page() {
  return (
    <>
      <h1>Nanostores Users Multitable</h1>
      <MyDatagrid title="Grid 1" />
      <MyDatagrid title="Grid 2" />
    </>
  );
}

const myDataGridStoreFactory = () => {
  const perPage = 3;
  const $page = atom(1);

  const $skip = computed($page, (page) => (page - 1) * perPage);

  const $fetcherStore = createFetcherStore<any>([
    `https://dummyjson.com/users?limit=`,
    perPage,
    "&skip=",
    $skip,
    "&select=firstName,age",
  ]);

  return {
    $page,
    setPage: $page.set,
    $fetcherStore,
  };
};

const MyDatagrid = ({ title }: { title: string }) => {
  /* 
    uses myDataGridStore factory
    store is local component instance `nanostores` stores collection

    // this does NOT work:
    const store = myDataGridStoreFactory();

    // this works:
    const [store] = useState(myDataGridStoreFactory());
  */
  // const store = myDataGridStoreFactory();
  const [store] = useState(myDataGridStoreFactory());

  // binds `nanostores` to react
  const page = useStore(store.$page);
  const fetcherStore = useStore(store.$fetcherStore);
  const { setPage } = store;

  return (
    <>
      <h2>{title || "DataGrid"}</h2>
      <h3>page: {page}</h3>
      <div>
        <button
          onClick={() => {
            // store.$page.set(page + 1);
            setPage(page + 1);
          }}
        >
          page +1
        </button>

        {fetcherStore.loading && <div>Loading</div>}
        {fetcherStore.error && <div>Error</div>}
        {fetcherStore.data && (
          <pre>{JSON.stringify(fetcherStore?.data, null, 2)}</pre>
        )}
      </div>
    </>
  );
};

[bug] create FetcherStore inside FC and it never get updates on react 18

Example from Local state and Pagination does not work with react 18
I've made a quick example on codesandbox to reproduce this issue

Problem description:

useStore($fetcherStore) never returns an updated data, thou network call is completed successfully and Promise resolves without an error
So my component never re-renders with an actual data

What happens

I debugged it down to the store.notify() call, after calling set in here
When notify got called it has no currentListeners or nextListeners here so no one gets notified about the state change

I've checked that this is the case only for react 18 with new batching mode ON, so my guess it's somehow connected to the Automatic Batching feature

It is possible that actual problem lies somewhere in the upstream @nanostores but I could not figure out where to look next

Hope you could help and clarify this issue, thanks for the great lib by the way!

Possible to implement something like useInfiniteQuery from @tanstack/query ?

Hi,
First of all, let me say this is a great library, and I'm loving it so far.

I am using query along with nanostores to build a framework-agnostic library, which I can then use to build components for each framework. All the business logic and reactivity stay in this "core" library.

TLDR;
Is it possible to do something like useInfiniteQuery in @tanstack/query?
https://tanstack.com/query/v5/docs/framework/react/reference/useInfiniteQuery
I want to build the whole infinite scroll data store inside VanillaJS so that I can reuse the same store in React, Vue, Angular etc...

Fake client-side idempotency

I think, given that FE forms/buttons can trigger the same mutation as many times as you want, it would probably be a good idea to add some client-side fake idempotency? Obviously we cannot change the way the API works, but we should at least protect the developer from calling the same mutation function multiple times.

So the idea is pretty straightforward: no simultaneous calls of one mutate function. If you call mutate, all subsequent calls to it will immediately return nothing and not trigger the mutation function until the initial mutation function resolves.

Add EM banner

Something like:

<a href="https://evilmartians.com/?utm_source=nanostores-query">
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg"
       alt="Sponsored by Evil Martians" width="236" height="54">
</a>

We need this banner for all open source projects especially grown from the projects.

`createFetcherStore` Making Request With Old Query Keys With Dynamic Params

Reproducible Example: https://codesandbox.io/p/sandbox/query-test-n8nq75?file=%2Fsrc%2FTest.tsx%3A10%2C3

Hi! I've implemented some wrapping code after looking through the suggestion from this issue: #16

Basically I have a wrapping hook like this (based on the README file in this repo about sending dynamic arguments):

import { useStore } from '@nanostores/react';
import { Store } from 'nanostores';
import { useMemo } from 'react';

export const useFetcher = <F extends Store>(fetcher: F, deps?: any[]) => {
  /*
    useMemo with deps instead of useState because I need `createFetcherStore`
    to react to arguments changing
  */
  const $store = useMemo(() => fetcher, deps || []);
  return useStore($store);
};

and my store:

  export const $getThings = (
    type: string,
  ) =>
    createFetcherStore(['my-things', type], {
      fetcher: () =>
        getThingsByType(type).then((response) => response.data),
    });

and my react component:

import { useFetcher } from "hooks/useFetcher";

const MyComponent = () => {
  const [type, setType] = useState("default-type");

  const { data, loading } = useFetcher($getThings(type), [type]);

  return <select onChange={setType} />;
};

Now, this all works perfectly. When I change the value in my select dropdown, it successfully makes the request with the updated data.

The unintended side-effect however, is that after 4 seconds (the time which the data is requested fresh), any change to my dropdown will trigger 2 requests: 1 with the old type and 1 with the new type

Video here. You can see 1 request goes off normally, but after 4 seconds, it will make 2 requests:

https://imgur.com/YZn4RE1

My question here is if there's any way to prevent the request from being fired with the old query key

Is there a way to persist the cache in the browser?

I just found out this amazing project! Congratulations!

In my dreams I would like to replace @tanstack/query and urql entirely with nanostores/query. It would be a crazy joy! 😄

The only thing that blocks me now is that I need to persist all the query data (the cache) in the browser (like I do with those libraries).

I was wondering if there is a way to persist the cahce using IndexedDB or something else (I prefer not to use localStorage which besides its already well known limitations also has the problem that it is sync and blocks the main thread).

@ai suggested me here to use https://developer.mozilla.org/en-US/docs/Web/API/Cache.

I need this to be able to restore data when there is no or intermittent internet (I'm already using PWA).

What do you think?

Once again: thank you for these magical projects!

Multiple re-renders | React

I have implemented a hook fetcher which takes the fetcher's data (@nanostores/query used) from useStore. However, I am observing with console log that the same data is logged multiple times causing a re-render in to the consumer components.

const useData = <T, E>(fetcher: FetcherStore<T, E>) => {
  const { data, error, loading: validating } = useStore(fetcher);
  console.log(data);

  const loading = validating && !data && !error;

  return {
    data,
    error,
    mutate: fetcher.revalidate,
    loading,
    validating,
    actions: {
      invalidate: fetcher.invalidate,
      revalidate: fetcher.revalidate,
      mutate: fetcher.mutate,
    },
  };
};

Add size examples

We have a line Small. 1.66 Kb. I am not sure that all users know the size of other libraries. How much it is better than react-query?

We can add a text like:

  1. 10x times smaller than react-query (with a link to https://bundlephobia.com/package/[email protected]). In US, it is OK to compare yourself with competitor.
  2. Or we can go to Around 10x smaller than alternatives if we do not want to add competitors.

Why Doesn't `mutate` return a value on Error or Promise Rejections?

Hi sorry for all the GitHub issues, I'm having another issue that i'm finding I may need to create a workaround for

I have code the roughly looks like this:

export const $createThing = createMutatorStore<CreateStreamPayload>(({ data, invalidate }) => {
  invalidate('things')
  return createThing(data);
})

...

const { mutate } = useStore($createThing)

const handleAddNewThing = async () => {
    try {
         const response = await mutate({ hello: 'world' });
         console.log(response) // only exist if non promise rejection
    } catch (e) {
       // never gets here
       showToastNotification('something went wrong!')
    }
}


<button onClick={handleAddNewThing} />

Now the issue here is that the only real way to show my toast notification is listen to error and do it in a useEffect like so:

export const $createThing = createMutatorStore<CreateStreamPayload>(({ data, invalidate }) => {
  invalidate('things')
  return createThing(data);
})

...

const { mutate, error } = useStore($createThing)

const handleAddNewThing = async () => {
    const response = await mutate({ hello: 'world' });
    console.log(response)
}

useEffect(() => showToastNotification('something went wrong!'), [error])

...

<button onClick={handleAddNewThing} />

So my question is, why can't I just get access to the error at the time I call mutate? Even react themselves say this is not a good way of reacting to changes:

https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers

Essentially what would be awesome is an option that will return the error inside this catch block:

https://github.com/nanostores/query/blob/main/lib/main.ts#L365

Test Usage example

The docs mention that the createFetcherStore / createMutatorStore global config should help with test setup and mocking of data, but, there's no practical example of what this looks like.

Do you happen to have a small example that shows how to do this in a test?

$store.set() not triggering the fetcher store

I have a fetcher that has a dependent value on another store.

const [createFetcherStore, createMutatorStore] = nanoquery({
    fetcher: async (...keys: (string | number)[]) => {
      const methodName = keys[0] as keyof Instance['notification'];
      if (methodName === 'get') {
        console.log('page no is...', keys[1]);
        return instance.notification.get({
          pageNo: keys[1] as number,
          limit: 3,
        });
      }
    },
  });
  
  const $currentPage = atom(1);
  const $notificationStore = createFetcherStore<Notification>([
    'get',
    $currentPage,
  ]);

I have a function in vanilla js loadMore()

    const loadMore = async () => {
     $currentPage.set($currentPage.get() + 1);
    };

I am trying to use these in vanilla js using the listen().

But calling loadMore() is not triggering the $notificationStore. Why?
Shouldn't the $currentPage store change trigger the $notificationStore ?

Weak typing when pass an object as a key

The type inference for the filters parameter in the $staffsData query is incorrect. It is inferred as SomeKey instead of the expected type RequestFiltersDTO.

Steps to Reproduce:

Define $formFilters atom and $filters computed:

export const $formFilters = atom<FormValue>({
  date: [dayjs().subtract(1, 'month'), dayjs()]
});

export const $filters = computed($formFilters, ({ date, ...formValues }) => {
  const payload: RequestFiltersDTO = formValues;
  if (date) {
    payload.date_from = date[0].startOf('day').toISOString();
    payload.date_to = date[1].endOf('day').toISOString();
  }
  return payload;
});

In this case, the $filters should be of type RequestFiltersDTO.

However, when examining the definition of the $staffData query, we encounter a TypeScript error:

const [query] = nanoquery({});

export const $staffsData = query([$filters], { // <-- TypeScript error here
  fetcher: (filters) => getAwesomeRequest(filters), // <-- `filters` is inferred as `SomeKey` instead of `RequestFiltersDTO`
});

Here, the $staffsData query has $filters defined as a dependency. However, the filters parameter in the fetcher function has an incorrect type inference.

`Cannot update a component while rendering a different component` Error When Using Fetcher In Multiple Places

Simplified reproducible example: https://codesandbox.io/p/sandbox/nanostoresreact-test-3wzp6z

To test:

  1. Open example codesandbox
  2. Click on select dropdown and select option
  3. See Cannot update a component while rendering a different component console error
Screenshot 2024-02-26 at 5 33 29 PM

I'm having an issue where I'm trying to follow the example from here in the README regarding sending parameters down, basically exactly as the docs recommend: https://github.com/nanostores/query?tab=readme-ov-file#local-state-and-pagination

The issue I'm having is when attempting to re-use the same fetcher in another component with the same query key and arguments, I get this error.

My actual use-case is as follows

  1. Load page. First useStore(store) runs
  2. Open pop-out drawer to add a new entity, which also needs to call the same useStore(store)
  3. Error appears because now 2 components are mounted that are calling the same nanostore

I've done some digging and it seems to be something related to useSyncExternalStore. Another data fetching library had a similar issue, which was documented here:

urql-graphql/urql#1382

They ended up just not using useSyncExternalStore, but I'm not 100% sure this issue is a problem with @nanostores/react because I tried my best to replicate without even using @nanostores/query and it didn't seem possible, which is why I'm logging it here.

There is also a few relevant react issues filed around this error, but they don't seem to be very helpful:

facebook/react#26962
facebook/react#18178

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.