npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deep-state-js

v1.0.2

Published

A reactive JS store that intelligently recalculates state based on dependency chains

Readme

deep-state-js

A reactive JS store that intelligently recalculates state based on dependency chains.

Contents

Installation

This module is distributed via npm and can be installed as a project dependency:

npm install --save deep-state-js

What is this?

A reactive state management tool built on dependency chains. Define the initial state and initial set of dependencies upfront and then allow deep-state-js to recalculate dependent state as updates are made to keys in the state.

A dependency is comprised of a key, a cond predicate function and a set of effects to be applied to the main key.

{
  A: {
    data: { value: 'I am A' },
  },
  B: {
    data: { value: 'I am B' },
    dependencies: [{
      // B is dependent on A (if A's data changes, we re-calculate B)
      key: 'A',
      // The `cond` gets the key's (i.e. A) data and returns a boolean
      cond: data => data.value === 'I am A',
      // If the `cond` evaluates to true, these
      // `effects` are applied on top of B's data
      effects: { value: 'Not B' }
    }]
  },
  C: {
    data: { value: 'I am C' },
    // If B updates as a result of a manual update or
    // dependency calculation then C re-calculates too
    dependencies: [{
      key: 'B',
      cond: data => data.value === 'Not B',
      effects: { value: 'Not C' }
    }]
  }
}

In the example above, C is dependent on B and B is dependent on A. In other words, any time A's data is updated, B is recalculated. In this case, B's dependency is true, so the effects are applied on top of B's data, so its value is Not B. The interesting thing is that the dependency chain is followed. Because B changed as a result of the dependency on A, C recalculates too because it's dependent on B and ends up with the value Not C.

An important note is that effects don't overwrite data, they're simply applied on top of the data. So if the value of A changes, then B would recalculate and its dependency would be false. In this case its value would be set back to what it originally was (i.e. I am B). Then because B changed, C would recalculate and go back to its original value I am C.

How To

The following gives a quick overview of how to interact with the deep-state-js store:

import { createStore } from 'deep-state-js';

const store = createStore({
  A: { data: { type: 'uppercase', value: '' } },
  b: { data: { type: 'lowercase', value: '' }, dependencies: [{ key: 'A', cond: (data) => data.value === 'Test', effects: { value: 'Test' } }] },
});

/* Update a value */
store.update('A', { value: 'Test' }); // Overwrites the data for `A`, so we lose the 'type'
store.update('A', (prev) => ({ ...prev, value: 'Test' })); // Uses the previous data for `A`, so we don't lose the 'type'

/* Reset the store state (the default value for the second argument is { data: true, dependencies: false }) */
// Only resets the data for the key `b` (overwrites all data)
store.reset({ b: { data: { value: 'b' } } }, { data: true, dependencies: false });
// Only resets the dependencies for the key `b`
store.reset({ b: { dependencies: [] } }, { data: false, dependencies: true });
// Only resets the data and dependencies for the key `b` (overwrites all data)
store.reset({ b: { data: { value: 'b' }, dependencies: [] } }, { data: true, dependencies: true });

/* Subscribe */
store.subscribe(() => {
  // Runs every time the store updates
  console.log(store.getSnapshot()); // Gets the current state of the store
});

Examples

Example: Jack and Jill

import { createStore } from 'deep-state-js';

const store = createStore({
  Jack: { data: { isUpTheHill: false } },
  Jill: { data: { isUpTheHill: false } },
});

Jack and Jill went up the hill...

store.update('Jack', { isUpTheHill: true });

Oh no! Where's Jill? No worries. She's on the way too...

store.update('Jill', { isUpTheHill: true });

A better way...

import { createStore } from 'deep-state-js';

const store = createStore({
  Jack: { data: { isUpTheHill: false } },
  Jill: {
    data: { isUpTheHill: false },
    dependencies: [
      {
        key: 'Jack',
        cond: (data) => data.isUpTheHill,
        effects: { isUpTheHill: true },
      },
    ],
  },
});

Jack and Jill went up the hill...

store.update('Jack', { isUpTheHill: true });

Jill is dependent on Jack, so she automagically follows...

Example: Basic Dependencies

Imagine a form with a subscribe to newsletter checkbox and an associated email text field. The email text field should only be enabled and have a value if the subscribe checkbox is checked (i.e. has a true value). Here's how you'd establish that relationship.

import { createStore } from 'deep-state-js';

const store = createStore({
  // Checkbox
  subscribeToNewsletter: { data: { value: true } },
  // Text Field
  email: {
    data: { value: '', disabled: false },
    dependencies: [
      {
        key: 'subscribeToNewsletter',
        cond: (data) => !data.value,
        effects: { value: '', disabled: true },
      },
    ],
  },
});

Example: Dependency Stacking

You may have noticed that the dependencies property is an array which means that multiple dependencies can be applied to a given key. This begs the question, what happens if dependencies clash. In this case, the order of the dependencies matters.

The dependencies stack. In other words, the dependencies that come later in the list are applied on top of those which come earlier in the list.

Let's take the example above and add another checkbox that subscribes to all emails. If that checkbox is checked then the subscribeToNewsletter checkbox should be ignored and the email text field should be re-enabled.

import { createStore } from 'deep-state-js';

const store = createStore({
  // Checkbox
  subscribeToAllEmails: { data: { value: true } },
  // Checkbox
  subscribeToNewsletter: { data: { value: true } },
  // Text Field
  email: {
    data: { value: '', disabled: false },
    dependencies: [
      {
        key: 'subscribeToNewsletter',
        cond: (data) => !data.value,
        effects: { value: '', disabled: true },
      },
      {
        key: 'subscribeToAllEmails',
        cond: (data) => !!data.value,
        effects: { disabled: false },
      },
    ],
  },
});

While the example above may work for the situation we described, it's not the best way to model that scenario. A better way would be to make the subscribeToNewsletter checkbox dependent on the subscribeToAllEmails checkbox and just disable it and make sure it's checked if the subscribeToAllEmails checkbox is checked. This has the added benefit of undoing the subscribeToNewsletter dependency on the email field and reverting the email field's value back to what it was before the effect was applied.

import { createStore } from 'deep-state-js';

const store = createStore({
  // Checkbox
  subscribeToAllEmails: { data: { value: true } },
  // Checkbox
  subscribeToNewsletter: {
    data: { value: true, disabled: false },
    dependencies: [
      {
        key: 'subscribeToAllEmails',
        cond: (data) => !!data.value,
        // Forcing the value to `true` reverts the dependency on the
        // email field because the cond predicate evaluates to `false`
        effects: { value: true, disabled: true },
      },
    ],
  },
  // Text Field
  email: {
    data: { value: '', disabled: false },
    dependencies: [
      {
        key: 'subscribeToNewsletter',
        cond: (data) => !data.value,
        effects: { value: '', disabled: true },
      },
    ],
  },
});

Example: Dynamic Effects

Until now, all the effects we've seen have been static objects that are applied on top of a given key's data. Effects have a secondary functional form which allows for a more dynamic experience.

Let's say that we wanted to keep the value of two keys in sync. We wouldn't be able to do this with a basic dependency because the effects object is fixed. Instead we can make the effects a function and generate the effects to apply dynamically. The effects function receives the same data that the cond predicate receives, which makes it perfect for this task.

import { createStore } from 'deep-state-js';

const store = createStore({
  MainKey: { data: { value: 'I am the main key' } },
  FollowerKey: {
    data: {},
    dependencies: [
      {
        key: 'MainKey',
        // In addition to functions, cond can be set directly
        // to true to force effects to always be applied
        cond: true,
        // The value of FollowerKey is set to the value of MainKey
        effects: (data) => ({ value: data.value }),
      },
    ],
  },
});

Example: Cyclic Dependencies

Let's go back to our example of Jack and Jill. If Jack goes up the hill first, then Jill should automatically follow. However, if Jill goes up the hill first, then Jack should automatically follow. To accomplish this, each person needs to be dependent on the other person. deep-state-js supports cyclic dependencies, so accomplishing this is trivial...

import { createStore } from 'deep-state-js';

const store = createStore({
  Jack: {
    data: { isUpTheHill: false },
    dependencies: [
      {
        key: 'Jill',
        cond: (data) => data.isUpTheHill,
        effects: { isUpTheHill: true },
      },
    ],
  },
  Jill: {
    data: { isUpTheHill: false },
    dependencies: [
      {
        key: 'Jack',
        cond: (data) => data.isUpTheHill,
        effects: { isUpTheHill: true },
      },
    ],
  },
});

Example: Composite Dependencies

Imagine a form with two fields that represent a range such as a start date and an end date. The start date is only valid if it's less than the end date and the end date is only valid if it's greater than the start date. In other words, this has the same problem as the previous example that each field is dependent on one another. However, there's a twist because not only do we need the data of the other field, but we need to be able to compare it with the current field's data.

This is where composite dependencies come into play. By passing an array of keys in a dependency, we can retrieve the data for every key in the array in the cond predicate function. This allows us to do complex calculations that span multiple keys in the state.

import { createStore } from 'deep-state-js';

const store = createStore({
  startDate: {
    data: { value: 'Jan. 1, 1900', valid: true },
    dependencies: [
      {
        key: ['startDate', 'endDate'],
        cond: (data) => data.startDate.value >= data.endDate.value,
        effects: { valid: false },
      },
    ],
  },
  endDate: {
    data: { value: 'Jan. 1, 2000', valid: true },
    dependencies: [
      {
        key: ['startDate', 'endDate'],
        cond: (data) => data.startDate.value >= data.endDate.value,
        effects: { valid: false },
      },
    ],
  },
});

Types

This package exports a createStore function with the following type:

type CreateStore = <Collection extends DataCollection = DataCollection>(configs: Configs<Collection>) => Store<Collection>;

The core types that make up createStore are as follows:

export type Data = Record<string, any>;

export type DataCollection = Record<string, Data>;

export type Configs<Collection extends DataCollection = DataCollection> = {
  [Key in keyof Collection]: {
    data?: Collection[Key];
    dependencies?: (
      | {
          [DependencyKey in keyof Collection]: {
            key: DependencyKey;
            keys?: never;
            cond: true | ((data: Collection[DependencyKey]) => boolean);
            effects: RecursivePartial<Collection[Key]> | ((data: Collection[DependencyKey]) => RecursivePartial<Collection[Key]>);
          };
        }[keyof Collection]
      | {
          key?: never;
          keys: (keyof Collection)[];
          cond: true | ((data: Collection) => boolean);
          effects: RecursivePartial<Collection[Key]> | ((data: Collection) => RecursivePartial<Collection[Key]>);
        }
    )[];
  };
};

export type Store<Collection extends DataCollection = DataCollection> = {
  getSnapshot(): Collection;
  reset(configs: Configs<Collection>, options?: { data?: boolean; dependencies?: boolean }): void;
  update(key: string, updater: Updater<Data>): void;
  subscribe(fn: () => void): () => void;
};

While the above might be somewhat confusing, the only thing that's really needed to create a typed output for the createStore function is a custom DataCollection type:

// A collection is a key value pair where the keys are the keys
// in your state and the values are the data for a given key
type CustomDataCollection = {
  KeyOne: { value: string };
  KeyTwo: { value: number };
};

const configs: Configs<CustomDataCollection> = {
  KeyOne: { data: { value: 'A string' } },
  KeyTwo: { data: { value: 1 } },
};

// Because `configs` is typed, `store` will be typed
const store = createStore(configs);