https://github.com/qpwo/dentata
Simple, fast, and fully-typed data tree with change listeners for node and the browser. Compile-time errors and autocomplete.
https://github.com/qpwo/dentata
barebones data-tree persistence state-management typescript
Last synced: 3 months ago
JSON representation
Simple, fast, and fully-typed data tree with change listeners for node and the browser. Compile-time errors and autocomplete.
- Host: GitHub
- URL: https://github.com/qpwo/dentata
- Owner: qpwo
- License: mit
- Created: 2021-12-30T23:44:30.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2022-05-12T19:32:35.000Z (about 3 years ago)
- Last Synced: 2024-12-30T23:32:09.469Z (5 months ago)
- Topics: barebones, data-tree, persistence, state-management, typescript
- Language: TypeScript
- Homepage: https://npmjs.com/package/dentata
- Size: 369 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# dentata: the American Chestnut of data trees
```bash
npm install dentata
yarn add dentata
``````js
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
```
- 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



## Longer example
This whole thing will run if you copy-paste it into node
```js
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.
```tsx
// 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)
returnYou are {username}.
}function AnotherButtonSomewhereElse() {
return usernameCursor.set('new username')}>Click
}// Or take a cursor as a prop
function Points(props: {points: Dentata}) {
returntotal pointage: {props.points.get()}// works
}
```## Compose cursors
```ts
// 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
```ts
class Dentata {
constructor(data: T);
// Get the current value at the cursor
get(): DeepReadonly;
// 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: 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): 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(key: K): Dentata;
// Alias for Dentata.select
s(key: K): Dentata;
// Listen for changes to the data at this cursor, including changes originating in parents or children.
onChange(handleChange: Listener): void;
// Remove all onChange listeners on this cursor
clearListeners(): void;
}// An onChange callback
type Listener = (newVal: DeepReadonly, oldVal: DeepReadonly) => void;// Alias for Dentata
const Dent: typeof Dentata;
type Dent = Dentata;// Return type of syntheticCursor
interface DentataLike {
get: () => DeepReadonly;
onChange: (l: Listener) => void;
}// Create a synthetic data cursor for computed values on another data cursor
function syntheticCursor(
fromCursor: DentataLike,
compute: (t: DeepReadonly) => OutputData,
settings?: { equality: "===" | "deep"; }
): DentataLike;// The equality algorithm, mainly exported so you can test it for your particular case
function deepEquals (a: unknown, b: unknown) => boolean;
```