Per component instance @nanostores/query without framework's useState()
zelid opened this issue · 2 comments
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>
</>
);
};
@zelid Yo!
Why
const [store] = useState(myDataGridStoreFactory());
works andconst 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.
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.