Library / SDK
LegendApp/legend-state avatar
LegendApp/legend-state

No releases, a beta version, and an example that reads the wrong variable

Legend-State is a super fast and powerful state library that enables fine-grained reactivity and easy automatic persistence

4,216 stars149 forksTypeScriptMIT

At a glance

What is it?
legend-state is a React state library with no GitHub releases at all: the only version is 3.0.0-beta.48 in package.json, published by eleven near-identical scripts that each run checks, bump a version and publish from a dist directory. The README is more interesting than that summary, though, because its first code sample reads a variable the sample never declares.
Who is it for?
The library's ideas are sound and its pitch is unusually specific: no actions, reducers or contexts, get and set on the data itself, fine-grained rendering through a memo component, and sync plugins for named backends. Judge it on two files. package.json is marked private, so the published artifact comes from a build directory and the version you install is whatever the last publish script pushed, not a tag you can point at.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 56 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 5, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The repository has no releases, and the version is a beta

There is nothing to compare. The release list for this repository is empty, so there is no tag to pin, no changelog entry on the releases page and no dated artifact to diff against. The only version anywhere in the tree is the manifest field, which reads `3.0.0-beta.48`.

That field is also the only thing that changes on a release, because versioning is done by the publishing scripts rather than by hand. Each of them runs the checks, bumps the version with npm, and publishes from the build directory under a distribution tag. The beta script runs `npm version prerelease --preid=beta` and then `npm publish --tag beta`, so the number of the beta is bumped and pushed by the pipeline.

The documentation is versioned separately from all of this, at URLs carrying a v3 path segment. So the prose describes a v3 line, the manifest describes beta 48, and the repository page lists no releases at all, which is three different version signals a reader has to reconcile.

Eleven publish scripts that differ by one word

The scripts section is where most of this repository's complexity lives. There are eleven publishing entries: manual, minor, alpharelease, betarelease, nextrelease, patch, premajor, preminor, prepatch, rcrelease, and one more after the point where the manifest stops.

Every one of them has the same three steps in the same order. Run the checks, bump a version, move into the build directory and publish. The difference between two scripts is one npm version argument and, for the prerelease ones, which distribution tag is passed to publish:

code
"publish:betarelease": "bun run check && npm version prerelease --preid=beta && cd dist && npm publish --tag beta",

The distribution tags in use are alpha, beta, next and rc, and the premajor, preminor and prepatch scripts all publish under next. So a consumer pinning a tag gets one of four moving targets, and none of them is a GitHub release.

The checks themselves are a single chain: lint, then a format check, then a silent test run.

The first example reads state$.settings.theme, and never declares it

The opening sample sets up its data like this:

jsx
import { observable, observe } from "@legendapp/state"
import { observer } from "@legendapp/state/react"

const settings$ = observable({ theme: 'dark' })

and then, a few lines later, inside the component, it reads `state$.settings.theme.get()`. That is a different variable with a different shape: the sample declared `settings$`, and nothing named `state$` exists anywhere above.

The same block also uses `<Memo>` inside the returned element without importing it, and the component is cut off at the line `retu`. So the first code block on the page is a shape rather than a program, which is a reasonable thing to know before you paste it into a file.

What the sample does show clearly is the three-part model: `get()` returns the raw data, `set()` changes it, and a computed observable is just a function of other observables, written as `observable(() => settings$.theme.get() === 'dark')`. An `observe` block re-runs when the observables it reads change.

The speed claim is one sentence wide and points elsewhere

The performance goal is stated in a single paragraph. It claims the library beats every other state library on just about every metric, that it is optimized for arrays well enough to beat vanilla JavaScript on the swap and replace-all-rows benchmarks, and that it weighs 4kb. A link labelled Fast points at a separate page for the details, and no table, number or methodology appears in the repository.

What is in the tree is a benchmarks directory and a testbundles directory, so the measurements have a place to live even though the summary page does not show them. The same paragraph also claims a reduction in boilerplate as a file size saving, which is a different kind of number from a bundle measurement and is presented alongside it.

The React-side claim is more concrete: rendering less, less often. The sample uses an interval that updates a value once every 600 milliseconds and wraps only the text in a memo component, so the component itself does not re-render when the value changes.

Sync is four named backends and a config that ends mid-sentence

Persistence is split in two. Local persistence plugins for the browser and for React Native are included in the package, and sync plugins exist for four backends: Keel, Supabase, TanStack Query, and plain fetch.

A synced collection is declared with a backend function and a config object, and the sample is complete up to the point where it stops:

js
const state$ = observable({
    users: syncedKeel({
        list: queries.getUsers,
        create: mutations.createUsers,
        update: mutations.updateUsers,
        delete: mutations.deleteUsers,
        persist: { name: 'users', retrySync: true },
        debounceSet: 500,
        retry: {
            infinite: true,
        },
        changesSince: 'last-sync',
    }),
    // direct link to my user within the users observable
    me: () => state$.users['myuid']
})

Four knobs carry the local-first behaviour: `retrySync` inside persist, a 500 millisecond debounce on set, an infinite retry, and a `changesSince` marker of last-sync so only what changed is sent. The trailing comment in the original says that calling get activates through to the collection and starts syncing, and the page stops there.

The library is used as the sync layer inside two shipping products, Legend and Bravely, which is the stated reason its feature set is broad for something that sets up in a few lines.

Bun builds it, Jest tests it, and the package is marked private

The manifest mixes four tools by role. The build runs through tsup via bunx and then a second script, `posttsup.ts`, presumably for the output files tsup does not write. The tests run on Jest with a jest config, not on the bun runtime the rest of the scripts use. Linting and formatting are separate checks chained in `check`, and hooks are configured in a lefthook file rather than in a husky directory.

Entry fields are written by hand: main points at `./index.js`, module at `./index.mjs` and types at `./index.d.ts`, with the files field set to a single glob. There are two tsconfig files, one for the general build and one named for ESM output, plus a babel configuration in two forms at the root.

Two manifest details matter more than the rest. The package is marked private, so the repository manifest is not what gets installed; publishing happens from inside the build directory. And the engines field requires Node 16.6 or newer and npm 8.11 or newer, which is the floor for anyone not using bun at all.

Agent instruction files sit next to a directory of pull requests

The root of the tree holds the entry points for each target, with separate files for the base package, React, the web build, React Native, sync, and a trace module, plus an index. The examples directory contains exactly one file, middleware.ts, and the rest of the tree is docs, src, tests, benchmarks and testbundles.

Then there are the directories that are not usually in a library: issues, plans and prs, each committed as a working folder rather than living on the platform. Alongside them sit an AGENTS.md and a CLAUDE.md, which are instruction files for coding agents rather than documentation for people, and a CHANGELOG.md that has no release page to point at.

None of that is a problem in itself, and for a project maintained by one person with a few published products behind it, keeping plans and pull request notes in the tree is a defensible choice. It does mean the repository is doing three jobs at once: the package source, the working record, and the instruction set for whatever tool writes the next commit.

Editorial conclusion

The library's ideas are sound and its pitch is unusually specific: no actions, reducers or contexts, get and set on the data itself, fine-grained rendering through a memo component, and sync plugins for named backends. Judge it on two files. package.json is marked private, so the published artifact comes from a build directory and the version you install is whatever the last publish script pushed, not a tag you can point at. And the README is written at speed, with a sample that references a variable it never creates, so treat every snippet as a shape rather than something to paste. If you need a versioned contract today, ask which tag corresponds to what you are about to depend on.

Frequently asked questions

what is legend state

A state and sync library for React, published as @legendapp/state under the MIT licence. Its four stated goals are ease of use with no boilerplate, actions or reducers, fine-grained reactivity so components re-render less, and a sync and persistence layer with plugins for local storage, Keel, Supabase, TanStack Query and fetch.

legend state vs redux

The repository makes no comparison. What it does state is that there are no actions, reducers, dispatchers, sagas, thunks or epics, that the data is not modified, and that reading and writing are two calls, get and set.

Is legend-state actually faster than other state libraries?

The README claims it beats every other state library on just about every metric and beats vanilla JavaScript on the swap and replace-all-rows benchmarks at 4kb, and links to a separate page for the details. No numbers appear in the repository README; benchmarks and testbundles directories do.

Does legend-state support Supabase and React Native?

Local persistence plugins for the browser and for React Native are included in the package, and the sync plugins named are Keel, Supabase, TanStack Query and fetch. A synced collection is declared with a backend function plus persist, debounceSet, retry and changesSince options.

How do I install legend-state?

Three commands are given: bun add @legendapp/state, npm install @legendapp/state, or yarn add @legendapp/state. The manifest requires Node 16.6 or newer and npm 8.11 or newer, and the repository publishes no GitHub releases.

Official sources

  1. Issues
  2. LegendApp/legend-state on GitHub
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/legendapp-legend-state.svg)](https://hysenlabs.com/projects/legendapp-legend-state)