Nano Stores Query

repository·main·Indexed 43 days ago

https://github.com/nanostores/query

A lightweight (<2Kb) data fetching and mutation library for Nano Stores. It provides SWR (stale-while-revalidate) caching, automatic revalidation, and optimistic updates designed to work outside of UI components. Version 0.3.4 includes features like conditional fetching, manual cache management via invalidateKeys and revalidateKeys, and support for React Native.

Tokens
5.6K
Snippets
15
Records
25
Agent score
56%

What's inside Nano Stores Query

  1. Understand Fetcher Store states

    main

    A fetcher store's state depends on whether it is the first fetch, if it has stale data in cache, or if it is currently loading.

    StateloadingdataerrorpromiseWhen
    Initial (no subscribers)false———Before any component subscribes
    Loading (no cache)true——PromiseFirst fetch, or after invalidate
    Loading (stale cache)truePrevious data—PromiseRefetch when cacheLifetime cache exists
    SuccessfalseT——Fetcher resolved
    ErrorfalsePrevious data or —E—Fetcher rejected
    Conditional fetch disabledfalse———Any key part is null/undefined/false
    Deduplicated (from cache)falseT——Same key fetched within dedupeTime

    Key behaviors:

    • invalidate wipes the cache entirely (returns to "Loading (no cache)").
    • revalidate marks the cache as stale (returns to "Loading (stale cache)" with previous data visible).
    • When the store loses all subscribers (onStop), it resets to the initial state.
  2. Understand Mutator Store states

    main

    Mutator stores track the lifecycle of a mutation call.

    StateloadingdataerrorWhen
    Initialfalse——Before mutate() is called
    Loadingtrue——mutate() called, awaiting result
    SuccessfalseResult—Mutation resolved
    Errorfalse—EMutation rejected

    Key behaviors:

    • mutate() always resets data and error before starting.
    • When throttleCalls is true (default), calling mutate() while already loading is a no-op.
    • When the store loses all subscribers, it resets to the initial state. Any in-flight mutation results are discarded.
  3. Understand the stale-while-revalidate caching model

    main

    Nano Stores Query uses the stale-while-revalidate methodology to provide a smooth UI. When a user navigates back to a previously visited resource, the library serves the cached (stale) data immediately instead of showing a loading spinner, while simultaneously fetching fresh data in the background.

    To implement this effectively in your UI, always check for the existence of data before checking loading. This ensures that if cached data exists, it is displayed during background revalidation.

    Cache Configuration Settings

    • dedupeTime: The duration a user must stay on a different resource before returning to the original resource triggers a new fetch.
    • cacheLifetime: The maximum age of a cache entry. If the time since the first visit exceeds this, the library stops serving stale data and shows a spinner instead.
    • revalidate: Forces dedupeTime to 0 for a specific key, ensuring the next trigger (like refetchOnInterval) calls the fetch function. If the user is currently on the page, they see the cached value during the reload.
    • invalidate: Completely removes the cache entry for a key. Users will see a spinner immediately upon the next access.
    // components/Post.tsx
    const Post = () => {
      const { data, error, loading } = useStore($currentPost);
    
      // Check data first to embrace stale-while-revalidate
      if (data) return <div>{data.content}</div>;
      if (error) return <>Failed to load</>;
      if (loading) return <>Loading...</>;
    
      return null;
    };
  4. Initialize Nano Stores Query with nanoqueryFactory

    main

    To use Nano Stores Query, you must first initialize the factory using nanoqueryFactory. This function accepts a PlatformCompat object which provides platform-specific hooks for visibility changes and network reconnection. The factory returns a nanoquery function used to configure global settings and create stores.

    import { nanoqueryFactory } from './lib/factory';
    
    // PlatformCompat provides isAppVisible, visibilityChangeSubscribe, and reconnectChangeSubscribe
    const nanoquery = nanoqueryFactory(platformCompatInstance);
    
    // Initialize with global settings
    const query = nanoquery({
      cacheLifetime: 60000,
      revalidateOnFocus: true,
      fetcher: myGlobalFetcher
    });
  5. Manually wipe or invalidate cache for dependencies

    main

    If a store depends on an external factor (like a userId or refreshToken) that is not part of the store's own key, you must manually invalidate the cache when that factor changes to prevent stale data from the previous user/session.

    Wipe all keys

    Use invalidateKeys with a predicate that returns true to clear the entire cache:

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

    Target specific store invalidation

    If a specific store depends on an outside factor, call its invalidate method directly:

    onSet($someOutsideFactor, $specificStore.invalidate)
  6. Setup Nano Query for React Native

    main

    Nano Query is fully supported in React Native.

    Revalidation on Reconnect

    To enable revalidateOnReconnect functionality, you must install the @react-native-community/netinfo package. If not installed, reconnection events will simply not trigger revalidation.

    Importing the library

    If your environment does not support package exports, import from the specific React Native entry point:

    import { nanoquery } from "@nanostores/query/react-native";
  7. Implement component-level data fetching (Local State)

    main

    While module-scoped stores are common, you can create store instances limited to a single component's lifecycle. This allows you to fetch data based on component props or state while still leveraging Nano Query's cache and refetching features.

    To do this, wrap your createFetcherStore call in a factory function that is invoked within a component's state initialization.

    const createStore = (id: string) => () =>
      createFetcherStore<{ avatarUrl: string }>(`/api/user/${id}`);
    
    const UserAvatar: FC<{ id: string }> = ({ id }) => {
      // Initialize the store instance within component state
      const [$user] = useState(createStore(id));
    
      const { data } = useStore($user);
      if (!data) return null;
    
      return <img src={data.avatarUrl} />;
    };
  8. Configure Nano Stores Query context

    main

    Before creating stores, use nanoquery() to define a context. This allows you to share a default fetcher implementation and general settings across all fetcher and mutator stores. This context also facilitates easy mocking for tests and stories.

    import { nanoquery } from '@nanostores/query';
    
    export const [createFetcherStore, createMutatorStore] = nanoquery({
      fetcher: (...keys) => fetch(keys.join('')).then((r) => r.json()),
    });
  9. Manage current key mutations and invalidations

    main

    Fetcher stores provide three specific properties to manage the currently active key (the key currently being used for fetching/mutating):

    1. fetcherStore.invalidate: Invalidates the current key.
    2. fetcherStore.revalidate: Revalidates the current key.
    3. fetcherStore.mutate: A function that accepts a new value to mutate the current key for the fetcher.
    4. fetcherStore.key: Holds the current key in its serialized string form.
  10. Create a MutatorStore with createMutatorStore

    main

    A MutatorStore is used to manage manual data mutations (e.g., POST/PUT requests). It provides a controlled way to execute side effects and subsequently invalidate or revalidate related cache keys.

    How it works:

    1. You provide a ManualMutator function.
    2. When store.mutate(data) is called, the mutator runs.
    3. Inside the mutator, you can use invalidate(keySelector) or revalidate(keySelector) to trigger updates in existing FetcherStores.
    4. You can also use getCacheUpdater(key) to perform optimistic updates by directly modifying the cache.
    import { createMutatorStore } from './lib/factory';
    
    const updateNameStore = createMutatorStore(
      async ({ data, invalidate }) => {
        const result = await api.updateName(data.name);
        // Invalidate the specific key used by the FetcherStore
        invalidate(['/api/user', data.id]);
        return result;
      }
    );
    
    // Trigger mutation
    updateNameStore.mutate({ id: 1, name: 'New Name' });
  11. Use createFetcherStore to fetch data

    main

    A fetcher store is a reactive Nano Store (an atom) that automatically fetches data whenever its keys change. You can pass strings, numbers, or other Nano Stores as keys. If any key part is a NoKey (null, undefined, void, or false), the fetcher will not execute (conditional fetching).

    import { atom } from 'nanostores';
    import { createFetcherStore } from './fetcher';
    
    export const $currentPostId = atom('');
    // The store will re-fetch whenever $currentPostId updates
    export const $currentPost = createFetcherStore<Post>(['/api/post/', $currentPostId]);