Skip to content
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

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

Closed
zelid opened this issue Jul 28, 2023 · 2 comments
Closed

Comments

@zelid
Copy link

zelid commented Jul 28, 2023

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>
    </>
  );
};
@dkzlv
Copy link
Member

dkzlv commented Jul 28, 2023

@zelid Yo!

Why const [store] = useState(myDataGridStoreFactory()); works and const store = myDataGridStoreFactory(); doesn't work?

Well that's very easy: useState hack guarantees that the resulting store has the same identity across renders. It's sort of like useMemo, but with higher guarantees (useMemo can essentially recalc itself from time to time). Your second version creates a store with a new identity on every render, which asyncronously gets its state in useStore, which provokes component rerender, which creates a new store, which… you get the point.

writes "the useState hack should rarely be used"

By that I meant that quite often the requirement of a local state can be worked out with different tools. E.g., if you in your own case have a fixed predictable amount of grids, you could easily precreate N fetcher stores ahead of time and pass in the i counter variable to pick a specific store out of an array.
But the hack does have a reason to live, that's true.

Also curious how can MyDatagrid use URL query-string params as "source of truth" to get "pageNumber" and "filters" from URL params?

Well, you're on the right track, tbh. The only thing your code misses is the reactivity from URL params, right? You need to find a way to update the $page atom so that it triggers the reactivity of the fetcher store. That's up to you and syncing your router with this atom. If I were you I'd use nanostores/router. It actually has a special $searchParams stores. You can get a certain param from it using a computed: computed($searchParams, params => params.page).

I hope that answers your question?


On a side-note, we have a WIP PR that'll introduce the concept of local contexts in the nanostores core. If you're interested, the PR itself features a code example of what's gonna be possible in near future (I hope we'll be able to merge it in August). That'll eliminate all the problems regarding local state and those pesky useState hacks as even though you'll have a module-scope fetcher store, its work will be isolated to a local context, so you'll be able to wrap your datagrid component into a local context and repeat it however many times, the state will be completely isolated and independent.

@zelid
Copy link
Author

zelid commented Jul 28, 2023

E.g., if you in your own case have a fixed predictable amount of grids, you could easily precreate N fetcher stores ahead of time and pass in the i counter variable to pick a specific store out of an array.

Thanks! That's an interesting idea actually. A bit tricky in my real use-case (user can add any known amount data-grid widgets on dashboard) but should be doable. I didn't think factory function need some kind of 'useMemo' and that indexed array of fetchers would allow to avoid using 'useState() hack'. Interesting if indexed store is the only way to with vue3/solid/svelte/anglular but with Context nanostores/nanostores#232 it should not matter actually.

I hope that answers your question?

Yep, if I understand you correctly computed: computed($searchParams, params => params.page) would work without issues in browser and on Node.js environment also need to set $router.open('/dashboard?grid1page=1&grid2page=3') somewhere manually during SSR according to https://github.com/nanostores/router#server-side-rendering

In my real case I use https://github.com/brillout/vite-plugin-ssr because of multi-framework support of SSR and it comes with it's own router for page navigation, but has some tricky example how to change the router as well.

Context should also be super-helpful for SSR data pre-fetching whenever real SSR is needed.

@zelid zelid closed this as completed Jul 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants