dentata
TypeScript icon, indicating that this package has built-in type declarations

1.2.2 • Public • Published

dentata: the American Chestnut of data trees

npm install dentata
yarn add dentata
const { Dentata } = require('dentata') // import { Dentata } from 'dentata'
// Objects, arrays, and functions are supported
const tree = new Dentata({arr: [1, 2, 3], x: 'foo', myCallback: () => {}})
const x = tree.select('x')
x.set('bar')
// You can make a cursor from a primitive value too
const num = new Dentata(5)
num.get() // 5
num.set(6)
num.onChange((next, last) => console.log('difference:', next - last))
num.apply(prev => prev + 1)
// There is no difference between a root cursor and a selected subcursor

annotated-chestnut-tree

  • Simple, lean, and fully-typed data tree library with change listeners for node and the browser, javscript or typescript. A state manager that keeps things simple, fast, and understandable. A minimalist/bare-bones alternative to baobab. (Not to mention redux, etc.)
  • Zero dependencies and 2.7kb gzipped.
  • It is fully synchronous so no surprises waiting for your changes to propagate, or passing callbacks to set, which avoids many errors in both UIs and APIs.
  • You make a tree/cursor with new Dentata(data) and just have get, set, apply(update: old => new), and onChange(handler). This is flexible enough to manage state server-side, with simple DOM-based apps, in react, or in libraries. A change event will only fire if the new data is actually different, and will always fire if anything at or below the cursor is different.
  • Values from get and apply and onChange are deeply immutable via typescript's readonly modifier. So if you are using typescript then you will never mess up your tree by accidentally modifying a return value.
  • Thanks to an optimized deep equality check, all of this is very fast. The diff is only taken on nodes that have children or listeners, so it is often avoided.
  • If your editor supports typescript well (e.g. vscode) then you also get auto-complete for keys and compile-time errors for invalid keys or values.

Auto-complete and compile-time errors

autocomplete-example

deep-autocomplete-example

bad-keys-example

Longer example

This whole thing will run if you copy-paste it into node

const { Dentata } = require('dentata')
// or:
// import { Dentata } from 'dentata';

// Make a new data tree. The root cursor is just like any other cursor.
const dentata = new Dentata({array: [5,6,7], nested: {objects: {are: 'fine'}}})

// Select some cursors inside the tree:
const arrayCursor = dentata.select('array')
// `s` is an alias for `select`
const areCursor = dentata.s('nested').s('objects').s('are')

// We'll just log changes to our cursors. More useful onChangers would update UI or trigger server actions or recalculate a value or whatever.
arrayCursor.onChange((next, last) => console.log('array changed from', last,  'to',  next))
areCursor.onChange((next, last) => console.log('are changed from', last,  'to',  next))
dentata.onChange((next, last) => console.log('entire tree changed from', last,  'to',  next))

// Listeners are not triggered if the data is equal according to Dentata.deepEquals
dentata.set({array: [5,6,7], nested: {objects: {are: 'fine'}}})

arrayCursor.apply(last => [...last, 8])
// log: array changed from [ 5, 6, 7 ] to [ 5, 6, 7, 8 ]
// log: entire tree changed from { array: [ 5, 6, 7 ], nested: { objects: { are: 'fine' } } } to { array: [ 5, 6, 7, 8 ], nested: { objects: { are: 'fine' } } }

arrayCursor.select(0).set(555)
// log: array changed from [ 5, 6, 7, 8 ] to [ 555, 6, 7, 8 ]
// log: entire tree changed from { array: [ 5, 6, 7, 8 ], nested: { objects: { are: 'fine' } } } to { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'fine' } } }

areCursor.set('okay')
// log: are changed from fine to okay
// log: entire tree changed from { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'fine' } } } to { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'okay' } } }

dentata.apply(d => ({...d, newKey: 'newVal'}))
// log: entire tree changed from { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'okay' } } } to { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'okay' } }, newKey: 'newVal' }

// setting a value to undefined releases all cursors and listeners:
dentata.set(undefined)
// (all three listeners fire)
dentata.set(null)
// (no listeners fire)

React example

No more passing val1, setVal1, val2, setVal2 through props! Just pass the cursor, or select it from the root, or export it as a constant. There's no render cycle, parent context, transpilation, daemon, etc, it's just a data tree.

// Write the appropriate hook for react, preact, vue, mithril, or whatever:
function useDentata(cursor) {
    const [val, setVal] = useState(cursor.get())
    cursor.onChange(next => setVal(next))
    return val
}
const usernameCursor = tree.select('username')
function User() {
    const username = useDentata(usernameCursor)
    return <h1>You are {username}.</h1>
}

function AnotherButtonSomewhereElse() {
    return <button onClick={() => usernameCursor.set('new username')}>Click</button>
}

// Or take a cursor as a prop
function Points(props: {points: Dentata<number>}) {
    return <div>total pointage: {props.points.get()}</div> // works
}

Compose cursors

// Use the helper:
import { syntheticCursor } from 'dentata'

const sumCursor = syntheticCursor(tree.select('numbers'), nums => nums.reduce((x, y) => x + y, 0))
const currentSum = sumCursor.get()
sumCursor.onChange(newSum => myDiv.innerText = `sum: ${newSum}`)
// synthetic cursors do not have `set` or `select`, naturally.

// Or you can roll your own:
function makeAreaCursor(rectangleCursor) {
    const listeners = []
    const areaOf = { width, height } => width * height
    return {
        get: () => areaOf(rectangleCursor)
        set: (newArea) => {
            const side = Math.sqrt(newArea)
            rectangleCursor.set({width: side, height: side})
        }
    }
}

const area = makeAreaCursor(rectangleCursor)
console.log(area.get())

Performance

Results from the "is reasonably fast" test in index.test.ts in node v17.4.0 on a 4-core 2015 macbook pro:

  • 100k separate trees in 0.063 seconds
  • Separately setting 100k values in a mixed-depth tree with about 100 nodes having cursors: 1.4 seconds
  • One 2k-node mixed-depth tree with cursors and onChange listeners on every node: 1.2 seconds
  • Making one tree all at once from a giant object is basically instant
  • For comparison, making a 100k-value plain object took 0.04 seconds and 100k function instantiations + calling took 0.04 seconds.

Contribution

Pull requests and new issues are welcome. I don't want to make it too complicated. If you want a big new feature then I recommend making a fork, or checking out something like baobab or redux. Please do file an issue right away if you notice a bug or performance problem

Full API

class Dentata<T> {
    constructor(data: T);
    // Get the current value at the cursor
    get(): DeepReadonly<T>;
    // Set data of current cursor and notify relevant onChange listeners. Set to `undefined` to remove all listeners and descendant cursors.
    set(newVal: T): void;
    // Set value at key
    setIn<K extends keyof T>(k: K, val: T[K]): void;
    // Alias for get + set. Update the old value into a new value. Do not mutate the argument.
    apply(update: (prev: DeepReadonly<T>) => T): void;
    // Get a cursor deeper into the tree. It will be notified of parent changes and will tell parent if it changes (if either has change listeners).
    select<K extends keyof T>(key: K): Dentata<T[K]>;
    // Alias for Dentata.select
    s<K extends keyof T>(key: K): Dentata<T[K]>;
    // Listen for changes to the data at this cursor, including changes originating in parents or children.
    onChange(handleChange: Listener<T>): void;
    // Remove all onChange listeners on this cursor
    clearListeners(): void;
}

// An onChange callback
type Listener<T> = (newVal: DeepReadonly<T>, oldVal: DeepReadonly<T>) => void;

// Alias for Dentata
const Dent: typeof Dentata;
type Dent<T> = Dentata<T>;

// Return type of syntheticCursor
interface DentataLike<T> {
    get: () => DeepReadonly<T>;
    onChange: (l: Listener<T>) => void;
}

// Create a synthetic data cursor for computed values on another data cursor
function syntheticCursor<InputData, OutputData>(
    fromCursor: DentataLike<InputData>,
    compute: (t: DeepReadonly<InputData>) => OutputData,
    settings?: { equality: "===" | "deep"; }
    ): DentataLike<OutputData>;

// The equality algorithm, mainly exported so you can test it for your particular case
function deepEquals (a: unknown, b: unknown) => boolean;

Readme

Keywords

none

Package Sidebar

Install

npm i dentata

Weekly Downloads

0

Version

1.2.2

License

MIT

Unpacked Size

96.8 kB

Total Files

13

Last publish

Collaborators

  • qpwo