nests

Fast and easy state storage with a lot of control.

Usage no npm install needed!

<script type="module">
  import nests from 'https://cdn.skypack.dev/nests';
</script>

README

Nests

Fast and easy state storage with a lot of control.



import * as nests from "nests";
const nest = nests.make();

nest.on(nests.Events.SET, (eventType, { path, value }) => {
    console.log(`set: ${path} = ${value}`);
});

nest.store.array = [1, 2, 3];
nest.store.array.push(4);
nest.store.instant.deeply.nested.state = true;

/* Console Output */
// set: array = 1,2,3
// set: array,3 = 4
// set: array,length = 4
// set: instant,deeply,nested,state = true

Installation

npm i nests

When importing you can specify nests/esm or nests/mjs if your bundler is giving you problems.

import * as nests from "nests";

Links

Features

Concepts

Store

The store is an object that lets you store data in an instantly deeply nested structure which automatically emits events to subscribers for you.

Instant Deeply Nested Objects

Have you ever had to do something like this?

const storage = {};

if (!storage.hasOwnProperty("foo")) {
    storage.foo = "bar";
}

With Nests this isn't necessary anymore.

const nest = nests.make();

nest.store.foo = "bar";

The code is even shorter and more readable.

You can go as deep as you want instantly without having to check if the property exists.

nest.store.foo.bar.baz = "qux";

Getting a property that doesn't exist will return {} instead of undefined. Use ghost to check for non-existent properties correctly.

nest.store.foo.bar.baz; // {}

Automatic Events

Nests comes with a small, custom, and fast, browser-ready EventEmitter.

nest.on(nests.Events.SET, (eventType, { path, value }) => {
    console.log(`set: ${path} = ${value}`);
});

nest.store.array = [1, 2, 3];
nest.store.array.push(4);
nest.store.instant.deeply.nested.state = true;

/* Console Output */
// set: array = 1,2,3
// set: array,3 = 4
// set: array,length = 4
// set: instant,deeply,nested,state = true

Ghost

The ghost is an object that is dual-synced with the store but isn't instantly deeply nested, doesn't emit events and is faster.

Each nest has a ghost property that can be used to modify the store's data without triggering events. This is super handy for doing heavy calculations with many mutations and creating transient React components easily.

The ghost of each nest is directly tied to the store so modifications to the ghost will be reflected in the store.

nest.on(nests.Events.SET, (eventType, { path, value }) => {
    console.log(`set: ${path} = ${value}`);
});

nest.ghost.array = [1, 2, 3];
nest.ghost.array.push(4);

nest.store.array.push(5);

console.log("The sync goes both ways:", nest.ghost.array);

/* Console Output */
// set: array,4 = 5
// set: array,length = 5
// The sync goes both ways: 1,2,3,4,5

If you want to trigger an event manually without using the store, you can use any of these functions.

// Use this.
nest.update();
// Using these is discouraged.
nest.get();
nest.set();
nest.delete();

The downfall of ghost is that it doesn't support Instant Deeply Nested Objects.

nest.ghost.foo.bar.baz = "qux"; // Error!

This is also a good thing. It allows you to check for non-existent properties unlike the store.

if (!nest.ghost.foo?.bar?.baz) {
    // ...
}

React

Here's an example of a React component.

import * as nests from "nests";
import { useNest } from "nests/react";

const settings = nests.make({
    enabled: true,
    name: "",
});

export default function App() {
    // Automatically subscribe to changes in the store.
    useNest(settings);

    return (
        <>
            <button
                onClick={() => {
                    settings.store.enabled = !settings.store.enabled;
                }}
            >
                {settings.ghost.enabled ? "Enabled" : "Disabled"}
            </button>
            <input
                type="text"
                value={settings.ghost.name}
                onInput={(event) => {
                    settings.store.name = event.target.value;
                }}
            />
        </>
    );
}

Here's an example of a transient React component.

import * as nests from "nests";
import { useNest } from "nests/react";

const counter = nests.make({
    count: 0,
});

setInterval(() => {
    // Increment using the ghost to not update the component.
    counter.ghost.count++;
}, 0);

export default function App() {
    // Automatically subscribe to changes in the store.
    // Pass true to indicate that it's a transient component.
    // Here we have a filter as well to only update the component when we want to.
    // That lets us avoid updating the component when a property we don't care about is changed.
    useNest(
        counter,
        true,
        (event, type) => event === "UPDATE" && type === "counter"
    );

    return (
        <>
            {counter.ghost.count}
            <button
                onClick={() => {
                    // Whenever the button is clicked, cause an update on the store.
                    // You can pass data to the update function which passes it to the filter in the useNest above.
                    counter.update("counter");
                }}
            >
                Update
            </button>
        </>
    );
}

Notice that whenever any data is displayed the ghost is used to retrieve it. This is because the ghost is faster and doesn't emit events.

Solid

Here's an example of a Solid component.

Notice the key difference between Solid and React's useNest hooks. While both hooks return the ghost, you're required to use the ghost the Solid hook returns in order for the component to update.

import * as nests from "nests";
import { useNest } from "nests/solid-js";

const settings = nests.make({
    enabled: true,
    name: "",
});

export default function App() {
    // Automatically subscribe to changes in the store.
    const signalGhost = useNest(settings);

    return (
        <>
            <button
                onClick={() => {
                    settings.store.enabled = !settings.store.enabled;
                }}
            >
                {signalGhost.enabled ? "Enabled" : "Disabled"}
            </button>
            <input
                type="text"
                value={signalGhost.name}
                onInput={(event) => {
                    settings.store.name = event.target.value;
                }}
            />
        </>
    );
}

Here's an example of a transient Solid component.

import * as nests from "nests";
import { useNest } from "nests/solid-js";

const counter = nests.make({
    count: 0,
});

setInterval(() => {
    // Increment using the ghost to not update the component.
    counter.ghost.count++;
}, 0);

export default function App() {
    // Automatically subscribe to changes in the store.
    // Pass true to indicate that it's a transient component.
    // Here we have a filter as well to only update the component when we want to.
    // That lets us avoid updating the component when a property we don't care about is changed.
    const signalGhost = useNest(
        counter,
        true,
        (event, type) => event === "UPDATE" && type === "counter"
    );

    return (
        <>
            {signalGhost.count}
            <button
                onClick={() => {
                    // Whenever the button is clicked, cause an update on the store.
                    // You can pass data to the update function which passes it to the filter in the useNest above.
                    counter.update("counter");
                }}
            >
                Update
            </button>
        </>
    );
}

Notice that whenever any data is displayed the ghost is used to retrieve it. This is because the ghost is faster and doesn't emit events.