Open-source project
heluxjs/helux avatar
heluxjs/helux

helux: an atom and signal state engine with dependency collection for React

A reactive atomic state engine for React(including React 18) and all React like, carrying dependency collection feature, supporting fine-grained updates.

1,388 stars69 forksTypeScriptMIT

At a glance

What is it?
helux is a state engine that puts atoms, signals, dependency collection, derivation and observation in one package, so a component re-renders when the specific node it read changes rather than when the object it came from changes. The migration from useState is a one-line import and a draft-based update.
Who is it for?
helux fits a React team that has a shared state problem a reducer cannot solve, where components subscribe to whole objects and re-render when one field changes, and where the team wants dependency collection at the engine level rather than through a component library.
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 111 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 November 6, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • heluxjs/helux1,388 ★ · -1
Star history
Repository2026-10-172026-11-06
heluxjs/helux1.4k1.4k

-1 Stars in the last 20 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

atom, signal, dependency collection in one package

helux is a state engine that brings `atom`, `signal`, dependency collection, derivation and observation together, and it supports fine-grained reactive updates across React and React-like frameworks, React 18 included.

The single-package framing is the point. Many state libraries ship a core plus a separate React binding, and the fine-grained part sometimes lives in its own package with its own version and rules. Here the atom, the signal and the dependency tracker are one install.

The feature list gives the shape of it. The dependency tracking is built in, and it is developed on top of limu, which the project describes as the fastest immutable JavaScript library, with the performance claim attached to that choice rather than to the engine alone. An `atom` accepts an arbitrary data structure and carries its own dependency collection, which means a state object does not have to be split into many small pieces, and the project argues that this keeps the design friendly to domain driven design, where an aggregate is one object rather than a bag of atoms.

The signal mechanism is the other half. It provides a response mechanism that updates at DOM or block granularity with no hook code at all, which is the part that is hard to get from a conventional store. There is a built-in `loading` module that manages the running state of asynchronous tasks and captures errors for components and plugins, a `sync` family of APIs for two-way binding and therefore for form handling, a `reactive` object type whose data changes drive the associated UI directly, `define` APIs for modularising state, and an event system.

Two kinds of derivation sit side by side. The mutable derivation responds to changes in some nodes of a shared object and produces values in other nodes of the same object, with a smaller update granularity, and the full derivation is for the cases that do not need fine-grained updates. Both support asynchronous tasks, and both can be triggered manually as well as by a data change.

There is a middleware and plugin system, and the project positions it as a way to connect to the redux ecosystem tooling. The code is 100 percent TypeScript, and the licence is MIT.

The migration from useState is one import

The quick start is a thirty second exercise, and the README frames it as promoting a local state to shared state.

Install with `npm i helux`, call `atom` to create the shared state, and call `useAtom` to use it. The diff in the README shows the change for a component that holds a count and a nested object:

diff
import React from 'react';
+ import { atom, useAtom } from 'helux';
+ const [sharedState] = atom({ a: 100, b: { b1: 1 } });

function HelloHelux(props: any) {
-   const [state, setState] = React.useState({ a: 100, b: { b1: 1, b2: 2 } });
+   const [state, setState] = useAtom(sharedState);

-   const change = setState((prev) => ({ ...prev, b: { ...prev.b, b1: 100 } }));
+   const change = setState((draft) => { draft.b.b1 = 100 });

  return <div>{state.a}</div>
}

Two things change and one thing does not. The state is created once, outside the component, and consumed through `useAtom` rather than `useState`. The update goes through a draft that is mutated in place, instead of a spread that rebuilds the object. And the rendering line is unchanged, which is the point: a component still reads `state.a` and the framework works out what that read depended on.

The draft is not a mutation of the live state. It produces a new state with the unchanged parts shared with the old one, which is the structural sharing that makes fine-grained updates affordable, and it is the reason the engine can be built on an immutable library and still let the user write `draft.b.b1 = 100`.

The atom API on its own is equally small:

tsx
import { atom, useAtom } from 'helux';
const [numAtom] = atom(1);

function Demo() {
  const [num, setAtom] = useAtom(numAtom);
  return <h1 onClick={setAtom(Math.random())}>{num}</h1>;
}

The atom is a box, `num` is unboxed automatically for the component, and the setter is passed straight to the click handler.

Dependencies are collected while the component renders

The mechanism that makes the engine different from a store with a selector is that the dependencies are collected during rendering, and the granularity is the node that was read.

The documentation example has two components reading the same atom. One reads `obj.a` and the other reads `obj.b.b1`, and an update to the draft of `obj.a` triggers a re-render of the first and not of the second.

tsx
function Demo1() {
  const [obj] = useAtom(objAtom);
  return <h1>{obj.a}</h1>;
}

function Demo2() {
  const [obj] = useAtom(objAtom);
  return <h1>{obj.b.b1}</h1>;
}

Nothing in that code declares a subscription. The dependency is inferred from the property access during the render, which is what lets the same atom serve components with completely different interests, and it is why the atom does not have to be split into small pieces: the granularity comes from the read, not from the shape of the state.

The cost of inference is that the read has to happen inside a tracked context, and that a component which reads a property conditionally collects only what it actually read on that render. Both are ordinary constraints of a tracking system, and both are the reason a component that reads a whole object still re-renders when any part of it changes.

The project also ships a separate repository, `how-helux-was-born`, whose stated purpose is to help users understand and debug the core logic of helux by walking through how it was built. For an engine that derives its behaviour from invisible tracking, that is a sensible thing to publish.

Signals update the DOM without a hook

The signal mechanism is the part of helux that is least like a conventional store, because the value is bound straight to the view.

tsx
<h1>{$(numAtom)}</h1>
<h1>{$(numAtom, num=>`hi helux ${num.val}`)}</h1>

The first form binds the raw value, and the documentation notes that it only causes a re-render inside the h1. The second takes a formatting function, so the same binding can present the value differently without the component owning the formatting.

The block form is the other half. A `block` declares a reactive region, and the dependencies are the nodes the block reads:

tsx
import { block } from 'helux';
const [objAtom] = atom({ a: 1, b: { b1: 1 } });

const UserBlock = block(() => (
  <div>
    <h1>{objAtom.a}</h1>
    <h1>{objAtom.b.b1}</h1>
  </div>
));

The dependency list for that block is `obj.val.a` and `obj.val.b.b1`, which is the point: a block updates because of the specific values it displays, and the surrounding component is not involved.

There is a subtlety in the documentation worth repeating, because it changes what a shared object means. If the object is created with `share` rather than `atom`, the dependencies of the block instance are `obj.a` and `obj.b.b1` instead. The difference is whether the values are boxed, and it is the kind of detail that decides how much re-rendering a shared object causes.

The whole feature set is described as zero hook encoding, with updates at DOM granularity or at block granularity, and the practical appeal is straightforward: a value that is displayed in one place does not re-render the tree around it.

Two kinds of derivation, and watch on top of them

The derivation API is split into a mutable form and a full form, and the difference is about what triggers the recomputation.

The mutable derivation responds to changes in some nodes of a shared object and produces values in other nodes of the same object:

ts
import { share, mutate } from 'helux';
const [shared] = share({ a: 1, b: 0, c: 0 });

mutate(shared)({
  changeB: (draft) => (draft.b = shared.a + 1),
  changeC: (draft) => (draft.c = shared.a + 2),
});

The same `mutate` call can also react to a different shared object, so a change in one shared state produces derived values in another. The full derivation is the alternative, where a change produces a whole new result:

ts
import { share, derive, deriveAtom, useDerived, useDerivedAtom } from 'helux';
const [shared] = share({ a: 1 });

const resultDict = derive(() => ({ num: shared.a + 100 }));

`derive` returns a dictionary-shaped result, `deriveAtom` returns an atom, and the two `use` variants unbox them for a component. The mutable form is the one that keeps the update granularity small, and the documentation is explicit that it exists for the case where part of a shared object changes and other parts of it should follow automatically.

On top of that sits `watch`, which is how a piece of code observes changes outside the render cycle:

ts
watch(() => {
  console.log('a change');
}, [shared.a]);

The dependency list is explicit here, unlike the render-time collection, and the callback is not executed on the first run. Passing the `immediate` option executes it right away and collects the dependencies automatically, which is the two ways the API offers and the difference between the two is the difference between a subscription list and a tracked read.

A Pinia-shaped wrapper, and how the project is built

For teams that want the shape of a familiar store library, there is a wrapper package, `@helux/store-pinia`, built on top of helux, and the README describes it as letting you manage React state the way you would with Pinia.

tsx
import { defineStore } from '@helux/store-pinia';

const counterStoreCtx = defineStore('Counter', {
  state: () => ({ count: 1, mountCount: 1 }),
  getters: {
    double() {
      return this.count * 2;
    },
  },

The getters carry the same property that the whole engine has. A getter derived from the state is not recomputed when its upstream dependencies do not change, and the example chains a second getter on top of the first, so the dependency tracking has to compose.

The rest of the store shape follows the familiar pattern, with actions that can be synchronous or asynchronous and a lifecycle hook that the framework calls. The documentation notes that the lifecycle methods are invoked by the framework and are not reachable from inside an action, and that this is enforced at the type level.

The repository is a pnpm workspace with Turborepo driving the build, and the scripts in the root `package.json` show the pipeline. The build runs through `turbo run build`, the release path is `release:auto`, which builds, versions, publishes and syncs, and versioning goes through changesets, with `changelog version` producing the changelog. There is a devtool package built separately with `build_devtool`, and the documentation is built by a docs package and copied into a separate pages repository by the `cpdoc` and `cpdoc2` scripts.

The quality tooling is conventional and worth noting because it is present in the file list: husky with a `prepare` hook, commitlint with a configuration file, prettier with its own configuration and a plugin file, lint-staged running prettier over the JavaScript, TypeScript, markdown, JSON and Less files, a `.changeset/` directory, and a `tests/` directory at the root.

The documentation is at `heluxjs.github.io/helux`, with a playground, and there are starter templates for React, OpenInula, Preact and Next.js, plus a repository called `how-helux-was-born` for debugging the source.

Where helux is the wrong tool

The case for a state engine is a codebase where re-rendering has become the problem, and the cost is a new mental model.

The first limit is scope. The engine is built around React and React-like frameworks. A Vue or Svelte application cannot use it, and a team that is not in that ecosystem has to look elsewhere. The claim in the README is that all React-like frameworks are supported, React 18 included, and the starter templates cover React, Preact, OpenInula and Next.js, which is a reasonable range, but it is a range inside one family.

The second is that this is a single-author project. The package metadata names one author, the community is organised around a WeChat group and a QQ group with the author, and the sponsorship section asks for support in the usual way. That is not a criticism, and the code is substantial, but a state engine is infrastructure, and infrastructure that one person maintains is a risk a company has to weigh. The related detail is that the project publishes no GitHub releases, so the version history lives in the changelog and in the changesets, and the version in the repository's `package.json` reads 5.4.6.

The third is the cost of the model itself. Dependency collection at render time means the engine is reasoning about your reads, which is also a level of indirection between the code and the behaviour. A team that debugs a rendering problem has to reason about what was tracked and when, and a team that has to explain the state layer to a new joiner has a larger onboarding task than with plain React state.

The alternative for a small or simple application is plain React state with a selector library, where the re-render behaviour is something the team controls explicitly rather than something the engine infers. The difference in approach is that helux takes the subscriptions out of the component code and puts them in the engine, which is the right trade when the state graph is large and the wrong trade when it is small, because the inference still costs something in bundle size, in build time and in debugging.

Editorial conclusion

helux fits a React team that has a shared state problem a reducer cannot solve, where components subscribe to whole objects and re-render when one field changes, and where the team wants dependency collection at the engine level rather than through a component library. It is the wrong choice for a small application that has a dozen components and a state object, where plain React state with a selector library is a smaller commitment, and it is the wrong choice for a team outside the React ecosystem, since the engine is built around React and React-like frameworks. Before adopting it, read the English README and the documentation site, and note that the package version in the repository is 5.4.6 while the project publishes no GitHub releases, so the version history lives in the changelog and the changesets rather than in tags.

Frequently asked questions

How do I install and start using helux?

Install it with `npm i helux`, create shared state with `atom` and consume it with `useAtom`, and the component keeps reading the same properties. The repository itself is a pnpm workspace built with Turborepo, with the packages in the `packages` directory and an older set in `packages-legacy`.

How does helux decide which component to re-render?

It collects dependencies while the component renders, so a component that reads obj.a re-renders only when obj.a changes, while one that reads obj.b.b1 re-renders only for that. Updates are written through a draft, which produces a new state while sharing the unchanged parts with the old one.

Does helux offer a Pinia-style store?

Yes, the @helux/store-pinia package provides defineStore with state, getters, actions and lifecycle, and its getters are recomputed only when their upstream dependencies change. The README notes that lifecycle methods are called by the framework and cannot be reached from inside an action.

Can helux update part of a page without a React hook?

Yes, the built-in signal mechanism binds a shared value directly to a view, so $(numAtom) only re-renders the inside of the h1, and block() defines a reactive region whose dependencies are the nodes it reads. A formatting function can be passed as well, for example $(numAtom, num=>...).

Official sources

  1. heluxjs/helux on GitHub
  2. Issues
  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/heluxjs-helux.svg)](https://hysenlabs.com/projects/heluxjs-helux)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/heluxjs-helux.svg)](https://hysenlabs.com/projects/heluxjs-helux)

Was this useful?