Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
@nanostores/query
Advanced tools
A tiny data fetcher for Nano Stores.
swr
or
react-query
, you'll get the same treatment,
but for 10-20% of the size.stale-while-revalidate
caching from
HTTP RFC 5861. User rarely sees unnecessary
loaders or stale data.npm install nanostores @nanostores/query
See Nano Stores docs about using the store and subscribing to store’s changes in UI frameworks.
First, we define the context. It allows us to share the default fetcher implementation between all fetcher stores, refetching settings, 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 (loading) return <>Loading...</>;
if (!data) return <>Error!</>;
return <div>{data.content}</div>;
};
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:
NoKey
, we never call the fetcher—this is the conditional fetching technique we have;SomeKey
and then transitioned to NoKey
, store's data
will be also unset;key
of the store.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 = 4s
dedupeTime?: number;
// If we should revalidate the data when the window focuses
// default = false
refetchOnFocus?: boolean;
// If we should revalidate the data when network connection restores
// default = false
refetchOnReconnect?: boolean;
// If we should run revalidation on an interval, in ms
// default = 0, no interval
refetchInterval?: number;
}
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
allows you to mark other keys as stale so they are refetched next time;getCacheUpdater
allows you to get current cache value by key and update it with
a new value. The key is also invalidated by default.export const $addComment = createMutatorStore<Comment>(
async ({ data: comment, invalidate, getCacheUpdater }) => {
// You can either invalidate the author…
invalidate(`/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>
);
};
You can also access the mutator function via $addComment.mutate
—the function is the same.
(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, mutateCache }] = nanoquery();
invalidateKeys
does 2 things:
It accepts one argument—the keys—in 3 different forms, that we call key selector.
// 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.
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.
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), ot mutate current key, right?
For these cases we have 3 additional things on fetcher stores:
fetcherStore.invalidate
. It's a function that invalidates current key for the fetcher. Doesn't accept any arguments.fetcherStore.mutate
. It's a function that mutates current key for the fetcher. Accepts the new value.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.
Let's say, you have a dependency for your fetcher, but you don't wish for it to be in your fetcher keys. For example, this could be your refreshToken
—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))
But 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)
FAQs
Tiny remote data fetching library for Nano Stores
We found that @nanostores/query demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.