Skip to content

[@xstate/store] Improve atom architecture #5250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Apr 19, 2025
Merged

Conversation

davidkpiano
Copy link
Member

  • Improved atom architecture with better dependency management (the diamond problem is solved!)

  • Optimized recomputation logic to prevent unnecessary updates

  • Added support for custom equality functions through compare option in createAtom, allowing fine-grained control over when atoms update:

    const coordinateAtom = createAtom(
      { x: 0, y: 0 },
      {
        // only update when x and y change
        compare: (prev, next) => prev.x === next.x && prev.y === next.y
      }
    );

Copy link

changeset-bot bot commented Apr 3, 2025

🦋 Changeset detected

Latest commit: d8374d6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@xstate/store Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@davidkpiano davidkpiano requested a review from Andarist April 3, 2025 04:54
@bowheart
Copy link

bowheart commented Apr 3, 2025

Hey, this is beautiful. Just wanted to give you a more official measuring stick for reactive competence than just the diamond problem.

Measuring Stick

Many regard this repo as the gold standard for testing reactivity: https://github.com/milomg/js-reactivity-benchmark

There's a fork of it that Alien Signals uses here: https://github.com/transitive-bullshit/js-reactivity-benchmark

I just cloned XState and plugged this branch (at the last passing commit) into the original js-reactivity-benchmark. I see several "Assertion failed" logs in all the benchmarks, including diamond. For reference, here's my implementation:

import { ReactiveFramework } from '../util/reactiveFramework'
import { createAtom } from '../../xstate/packages/xstate-store/src/atom'

export const xStateStoreFramework: ReactiveFramework = {
  name: 'XState Store',
  signal: initialValue => {
    const s = createAtom(initialValue)
    return {
      write: v => s.set(v),
      read: () => s.get(),
    }
  },
  computed: fn => {
    const c = createAtom(fn)
    return {
      read: () => c.get(),
    }
  },
  effect: fn => createAtom(fn),
  withBatch: fn => fn(),
  withBuild: fn => fn(),
}

Do You Need This?

No. This is technically for signals libs. While atomic libs typically have all the functionality of signals, they don't have to be valid signals implementations. In fact, Recoil and Nanostores are incapable of running these benchmarks. Jotai's latest version also can't run them, though earlier versions could get through all but the triangle benchmark.

But since your API is compatible, I would at least develop the initial implementation with this as an extra test suite. Making sure Zedux passes all these benchmarks has given us a lot of confidence that it's actually working and can handle all situations well. Unless I implemented it wrong, the fact that the diamond benchmark fails some assertions tells me this PR is probably missing something.

@davidkpiano
Copy link
Member Author

Thanks @bowheart! I'll take a look at these benchmarks and see how many of these we can get to pass.

@@ -385,7 +394,9 @@ export type AnyAtom = Atom<any>;
* atom.set(43);
* ```
*/
export interface ReadonlyAtom<T> extends Readable<T> {}
export interface ReadonlyAtom<T> extends BaseAtom<T>, Dependency, Subscriber {
update(): boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should update be on the atom? I thought it would be exclusive to computeds

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is exclusive to computeds (ReadonlyAtom is the computed atom)

Comment on lines 4 to 9
export interface Dependency {
/** @internal */
_subs: Link | undefined;
/** @internal */
_subsTail: Link | undefined;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see those types being directly exposed outside of this package but be careful when marking everything as @internal on a type. That will become an empty interface in the emitted declaration and you don't want that.

export interface Atom<T> extends Subscribable<T>, Readable<T> {
export interface BaseAtom<T> extends Subscribable<T>, Readable<T> {}

export interface InternalBaseAtom<T> extends Subscribable<T>, Readable<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those Internal* types are leaking as part of the public exports of the package right now because we just blindly reexport everything contained in this file. Ideally this would get fixed

@davidkpiano davidkpiano merged commit a1bffb5 into main Apr 19, 2025
1 check passed
@davidkpiano davidkpiano deleted the davidkpiano/atom-diamond branch April 19, 2025 21:29
@github-actions github-actions bot mentioned this pull request Apr 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants