Introducing Stan

Stan (Polish for "state") is another state management library for TypeScript that implements the atomic approach. Its core promise is:
Minimal, type-safe state management
I could probably end here, but there's the question: why? Indeed, why create yet another library when there are already so many? Fair enough - let me try to explain.
ℹ️ The source code for Stan is available on GitHub. You can install it using your favorite package manager. There's also a website with API docs and a bunch of examples.
The goal was to create a library that sits somewhere between Jotai and Recoil - specifically, not one or the other. In particular, the following qualities of both prompted the development of Stan:
Caching
Jotai isn't really concerned with caching - it only stores the most recent value, which, I imagine, can be sufficient in certain scenarios. While there is a way to alter that behavior, the code lives in an external package. There are also utilities like atomFamily
, which inherit typical memory management issues - issues Jotai tries to handle gracefully.
Recoil takes a very different approach - it caches aggressively. Boy, does it cache aggressively. Not only does it store everything by default (you have to opt out), but it also computes cache keys by serializing entire values. That can quickly get out of hand when dependencies update frequently (I've seen it happen).
I didn't find either of these approaches optimal. After working with atomic state for several years, I knew the answer lay somewhere in between. I wanted a design that addressed the problems I had encountered more effectively.
Bloat
Both Jotai and Recoil suffer from a form of bloat. With Jotai, you're lured in by the lean 2KB core, only to realize there's an entire ecosystem of plugins you'll realistically need to stay productive. Recoil, on the other hand - a chunky beast - bundles all of that in from the start. Don't get me wrong: all those extra APIs, features, and extensions surely have a project or person out there that needs them. Thing is, I don't.
In my experience, problems are better solved through flexible design than through obscure library features - especially the kind that, over time, make you increasingly dependent on the library vendor. Actually, speaking of that...
React lock-in
I don't see being heavily tied to React's reconciliation and Suspense mechanism as an advantage. An ideal setup - again, subjectively - is a framework-agnostic core with opt-in bindings for specific frameworks. As it happens, that’s exactly what Stan does. 😉
Recoil is dead
Officially dead. The last contribution was over two years ago, and the repository was archived on January 1, 2025. R.I.P.
This is exactly why the fear of vendor lock-in is rational. Sooner or later, some project gets killed. Whether that results in a massive migration cost - that’s up to you.
ℹ️ All the code samples below use @rkrupinski/stan
in version 1.3.1
.
Having established the motivation, let's now take a look at some code. We'll start simple - by fetching user data.
import { selectorFamily } from '@rkrupinski/stan';
const userById = selectorFamily<Promise<User>, string>(
userId => () => getUser(userId),
);
Let's now make sure to cancel redundant requests:
const userById = selectorFamily<Promise<User>, string>(
userId =>
({ signal }) =>
getUser(userId, { signal }),
);
The time has come for some memory management - let's store data for only the 5 most recently used user IDs:
const userById = selectorFamily<Promise<User>, string>(
userId =>
({ signal }) =>
getUser(userId, { signal }),
{
cachePolicy: {
type: 'lru',
maxSize: 5,
},
},
);
Finally, we're only interested in the user's name:
const userNameById = selectorFamily<Promise<string>, string>(
userId =>
async ({ get }) => {
const { name } = await get(userById(userId));
return name;
},
);
Up to this point, everything we've done has been framework-agnostic. Not a single line in the code above assumes it's running in a specific context, such as React. But ultimately, we do want to render the data - so let's use Stan's React bindings:
import { useStanValueAsync } from '@rkrupinski/stan/react';
const MyComponent: FC<{ userId: string }> = ({ userId }) => {
const result = useStanValueAsync(userNameById(userId));
switch (result.type) {
case 'loading':
return <p>Loading…</p>;
case 'error':
return <p>Nope</p>;
case 'ready':
return <p>Name: {result.value}</p>;
}
};
For more, see:
The photo is a fragment of The Creation of Adam, with a touch of AI.