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 🙏

© 2024 – Pkg Stats / Ryan Hefner

preact-observables

v0.0.7

Published

`preact-observables` enables the use of deep observable objects for `@preact/signals` supporting objects, arrays, `Map`, `Set`, `WeakMap` and `WeakSet`.

Downloads

4

Readme

preact-observables

preact-observables enables the use of deep observable objects for @preact/signals supporting objects, arrays, Map, Set, WeakMap and WeakSet.

Example

When an observable object is created each property is represented by a preact signal, getters are automatically wrapped in a computed and setters and methods are performed in a batch.

import from '@preact/signals'; // or '@preact/signals-react'
import {observable} from 'preact-observables';

const store = observable({
    get totalCompleted() {
        return store.todos.filter(todo => todo.completed).length;
    },
    addTodo(title) {
        store.todos.push({title: newTodo, completed: false})
    },
    removeTodo(index) {
        store.todos.splice(index, 1);
    },
    todos: [
        {title: "drink a cup of coffee", completed: true},
        {title: "get some fresh air", completed: false}
    ]
});

function Todos() {
    const [newTodo, setNewTodo] = useState();

    return (
        <div>
            <h1>Todos</h1>
            <div>
                <label>Create:</label>
                <input value={newTodo} onChange={e => setNewTodo(e.target.value)} />
                <button onClick={() => {
                    store.addTodo(newTodo);
                    setNewTodo('');
                }}>Add</button>
            </div>
            <ul>
             {store.todos.map((todo, index) => (
                <li key={todo.title}>
                    <checkbox value={todo.completed} onChange={e => todo.completed = e.target.checked}/>
                    <span>{todo.title}</span>
                    <button onClick={() => store.removeTodo(index)}>delete</button>
                </li>
            ))}
             <ul>
             <h2>Total Completed: {store.totalCompleted}</h2>
        </div>
    )
}

Using Classes

preact-observables also allows you to model your data with classes. Extend from the Observable base class and every property will be a signal, getters will be computed and methods/setters will be batched.

import { Observable } from "preact-observables";
import { effect } from "@preact/signals-core";

class Person extends Observable {
  firstName;
  lastName;

  constructor(firstName, lastName) {
    super();

    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  setName(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

const person = new Person("Alice", "Smith");

effect(() => {
  console.log(person.fullName);
});

// logs "Alice Jones"
person.setName("Alice", "Jones");

Working with Signals

Each observable is made up of multiple signals. A signal is created for each property and computed getters. Signals that represent a primitive type (string, number, etc) can be passed down directly on attributes and text elements and when those signals change only the individual DOM nodes need to be updated.

You can retrieve the signal from an observable object with the getSignal function:

const todos = observable(["drink coffee", "take a walk"]);

function Todos() {
  return todos.map((_, index) => <div>{getSignal(todos, index)}</div>);
}

todos[1] = "stay at home"; // now only a single div element will update instead of the entire component

As a convenience you can also use the $prop syntax to access signals on objects:

const todos = observable([
  { title: "drink coffee", completed: true },
  { title: "take a walk", completed: false },
]);

function Todos() {
  return todos.map((todo) => <div>{todo.$title}</div>);
}

todos[1].title = "stay at home"; // now only a single div element will update instead of the entire component

Note: If you already have a property that starts with $ in your object (eg: $myProp) you can access the signal using $$ (eg: $$myProp).

typescript Note: Due to typescript limitations all $ properties have a type of Signal<*> | undefined even though they will return a Signal at runtime. You will need to use ! to bypass this. Though if you're only passing them to text nodes and as attributes ! is not required

Integration with Preact / React

preact-observables work with @preact/signals-core if you wish to integrate it with preact you will need to import from '@preact/signals' somewhere in your code. Likewise with React you'll need to import from '@preact/signals-react'.

Observables under the hood

Observables and their underlying signals are always lazily initialized upon access. They are initialized from the source object that is passed into observable. The source object is then permanently associated to a single observable reference. Observing it again will return the same observable reference.

const obj = { value: "prop" };
observable(obj) === observable(obj); // true

Mutating the observable will also mutate the source but preact-observable will never write observable values back to the source object.

const obj = { inner: null };
const inner = { value: "prop2" };

const observableObj = observable(obj);
const observableInner = observable(inner);

observableObj.inner = observableInner;

obj.inner === observableObj.inner; // false
obj.inner === inner; // true
observable(obj.inner) === observableInner; // true

This behavior makes preact-observables ideal for observing existing/shared objects as they will not be mutated when making them observable nor will they ever get into a state where the original source object has both observable and non-observable values.

Observable Array performance

Many deeply observable proxy implementations use the same implementation to deal with both objects and arrays. The consequence of doing so means accessing Array.prototype methods will run entirely on the proxied array which results in orders of magnitude slower performance when compared to calling those same methods on a native array.

preact-observables utilizes the underlying source to execute common Array.prototype methods. This is purely an implementation detail but what it amounts to is performance that is closer to calling those methods on a native array.

Performance escape hatches

When dealing with observable objects there's an added overhead for every read and write operation that is performed as well as the overhead that's introduces by reading/writing to signals. While this overhead is significant it rarely becomes a performance issue as in typical web applications data processing of state/domain objects is not the bottle neck. Yet there might be situations where heavy data manipulation is required and doing so on an observable proxied object will be significantly slower then working with plain JavaScript objects.

preact-observables offers a performance escape hatch in these situation. Since observables are proxied shells over the original source we can modify the source object directly and then manually signal that the observable has been changed so that all reactions that depend on it can be ran. This can be achieved using reportChanged.

import { source, reportChanged } from "preact-observables";
import {effect} from "@preact/preact-signals";

const bigObj = observable(createAnExpensiveObject());

effect(() => {
    console.log(Object.keys(bigObj).length);
});

// get the original plain object source for this observable
const bigObjSource = source(bigObj);
// perform expensive mutations on the plain javascript object (fast)
performExpensiveMutations(bigObjSource);
reportChanged(bigObj); // manually signal to reactions that our object has changed and those reactions that depend on it need to re-run

preact-observables also exports reportObserved which is the read equivalent to reportChanged. It can be used when performing an expensive derivation within a reaction. reportObserved can also deeply observe all nested objects with reportObserved(obj, {deep: true})

API

observable(obj)

Return an observable proxy for a given object. Can be a plain object, array, map, set, date, weakmap or weakset. Resulting proxy will be deeply observed.

source(obj)

Returns the original source from an observable object.

isObservable(obj)

Returns a true if the passed in object is observable and false otherwise

getSignal(obj, key)

Returns the underlying preact signal or computed from an observable object

reportChanged(obj)

Force a change on the observable object so that any effects that depend on it can re-run

reportObserved(obj, options?: {deep?: boolean})

Force an observation on the observable object so that it can be added to an active reaction.