{"id":18869081,"url":"https://github.com/qpwo/dentata","last_synced_at":"2025-10-23T22:36:28.392Z","repository":{"id":92851315,"uuid":"443208677","full_name":"qpwo/dentata","owner":"qpwo","description":"Simple, fast, and fully-typed data tree with change listeners for node and the browser. Compile-time errors and autocomplete. ","archived":false,"fork":false,"pushed_at":"2022-05-12T19:32:35.000Z","size":378,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-08-09T04:33:34.480Z","etag":null,"topics":["barebones","data-tree","persistence","state-management","typescript"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/dentata","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qpwo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-30T23:44:30.000Z","updated_at":"2022-02-02T18:03:38.000Z","dependencies_parsed_at":null,"dependency_job_id":"b003fb3f-d4a0-4bb2-931e-2b90054676d0","html_url":"https://github.com/qpwo/dentata","commit_stats":{"total_commits":54,"total_committers":2,"mean_commits":27.0,"dds":0.07407407407407407,"last_synced_commit":"08a973f661cb52c4627a06889791ffd3f79e6457"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/qpwo/dentata","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qpwo%2Fdentata","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qpwo%2Fdentata/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qpwo%2Fdentata/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qpwo%2Fdentata/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qpwo","download_url":"https://codeload.github.com/qpwo/dentata/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qpwo%2Fdentata/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272063207,"owners_count":24866673,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-25T02:00:12.092Z","response_time":1107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["barebones","data-tree","persistence","state-management","typescript"],"created_at":"2024-11-08T05:15:40.983Z","updated_at":"2025-10-23T22:36:23.372Z","avatar_url":"https://github.com/qpwo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dentata: the American Chestnut of data trees\n\n```bash\nnpm install dentata\nyarn add dentata\n```\n\n```js\nconst { Dentata } = require('dentata') // import { Dentata } from 'dentata'\n// Objects, arrays, and functions are supported\nconst tree = new Dentata({arr: [1, 2, 3], x: 'foo', myCallback: () =\u003e {}})\nconst x = tree.select('x')\nx.set('bar')\n// You can make a cursor from a primitive value too\nconst num = new Dentata(5)\nnum.get() // 5\nnum.set(6)\nnum.onChange((next, last) =\u003e console.log('difference:', next - last))\nnum.apply(prev =\u003e prev + 1)\n// There is no difference between a root cursor and a selected subcursor\n```\n\n![annotated-chestnut-tree](https://user-images.githubusercontent.com/10591373/152053585-4b392b90-af82-44d2-ad46-fc7c39c560cb.jpg)\n\n- 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.)\n- **Zero dependencies and 2.7kb gzipped.**\n- 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.\n- You make a tree/cursor with `new Dentata(data)` and just have `get`, `set`, `apply(update: old =\u003e 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.\n- 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.\n- 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.\n- 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.\n\n## Auto-complete and compile-time errors\n\n![autocomplete-example](https://user-images.githubusercontent.com/10591373/152046346-fe840b8a-7916-4873-92ad-8b4459fb381c.png)\n\n![deep-autocomplete-example](https://user-images.githubusercontent.com/10591373/152046523-861a5860-1a45-4e3b-a412-257e56ea370d.png)\n\n![bad-keys-example](https://user-images.githubusercontent.com/10591373/152046307-0e0f8884-f2cb-4434-82d9-1cf151e23fa8.png)\n\n## Longer example\n\nThis whole thing will run if you copy-paste it into node\n\n```js\nconst { Dentata } = require('dentata')\n// or:\n// import { Dentata } from 'dentata';\n\n// Make a new data tree. The root cursor is just like any other cursor.\nconst dentata = new Dentata({array: [5,6,7], nested: {objects: {are: 'fine'}}})\n\n// Select some cursors inside the tree:\nconst arrayCursor = dentata.select('array')\n// `s` is an alias for `select`\nconst areCursor = dentata.s('nested').s('objects').s('are')\n\n// We'll just log changes to our cursors. More useful onChangers would update UI or trigger server actions or recalculate a value or whatever.\narrayCursor.onChange((next, last) =\u003e console.log('array changed from', last,  'to',  next))\nareCursor.onChange((next, last) =\u003e console.log('are changed from', last,  'to',  next))\ndentata.onChange((next, last) =\u003e console.log('entire tree changed from', last,  'to',  next))\n\n// Listeners are not triggered if the data is equal according to Dentata.deepEquals\ndentata.set({array: [5,6,7], nested: {objects: {are: 'fine'}}})\n\narrayCursor.apply(last =\u003e [...last, 8])\n// log: array changed from [ 5, 6, 7 ] to [ 5, 6, 7, 8 ]\n// log: entire tree changed from { array: [ 5, 6, 7 ], nested: { objects: { are: 'fine' } } } to { array: [ 5, 6, 7, 8 ], nested: { objects: { are: 'fine' } } }\n\narrayCursor.select(0).set(555)\n// log: array changed from [ 5, 6, 7, 8 ] to [ 555, 6, 7, 8 ]\n// log: entire tree changed from { array: [ 5, 6, 7, 8 ], nested: { objects: { are: 'fine' } } } to { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'fine' } } }\n\nareCursor.set('okay')\n// log: are changed from fine to okay\n// log: entire tree changed from { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'fine' } } } to { array: [ 555, 6, 7, 8 ], nested: { objects: { are: 'okay' } } }\n\ndentata.apply(d =\u003e ({...d, newKey: 'newVal'}))\n// 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' }\n\n// setting a value to undefined releases all cursors and listeners:\ndentata.set(undefined)\n// (all three listeners fire)\ndentata.set(null)\n// (no listeners fire)\n```\n\n## React example\n\n**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.\n\n```tsx\n// Write the appropriate hook for react, preact, vue, mithril, or whatever:\nfunction useDentata(cursor) {\n    const [val, setVal] = useState(cursor.get())\n    cursor.onChange(next =\u003e setVal(next))\n    return val\n}\nconst usernameCursor = tree.select('username')\nfunction User() {\n    const username = useDentata(usernameCursor)\n    return \u003ch1\u003eYou are {username}.\u003c/h1\u003e\n}\n\nfunction AnotherButtonSomewhereElse() {\n    return \u003cbutton onClick={() =\u003e usernameCursor.set('new username')}\u003eClick\u003c/button\u003e\n}\n\n// Or take a cursor as a prop\nfunction Points(props: {points: Dentata\u003cnumber\u003e}) {\n    return \u003cdiv\u003etotal pointage: {props.points.get()}\u003c/div\u003e // works\n}\n```\n\n## Compose cursors\n\n```ts\n// Use the helper:\nimport { syntheticCursor } from 'dentata'\n\nconst sumCursor = syntheticCursor(tree.select('numbers'), nums =\u003e nums.reduce((x, y) =\u003e x + y, 0))\nconst currentSum = sumCursor.get()\nsumCursor.onChange(newSum =\u003e myDiv.innerText = `sum: ${newSum}`)\n// synthetic cursors do not have `set` or `select`, naturally.\n\n// Or you can roll your own:\nfunction makeAreaCursor(rectangleCursor) {\n    const listeners = []\n    const areaOf = { width, height } =\u003e width * height\n    return {\n        get: () =\u003e areaOf(rectangleCursor)\n        set: (newArea) =\u003e {\n            const side = Math.sqrt(newArea)\n            rectangleCursor.set({width: side, height: side})\n        }\n    }\n}\n\nconst area = makeAreaCursor(rectangleCursor)\nconsole.log(area.get())\n```\n\n## Performance\n\nResults from the \"is reasonably fast\" test in `index.test.ts` in node v17.4.0 on a 4-core 2015 macbook pro:\n\n- 100k separate trees in 0.063 seconds\n- Separately setting 100k values in a mixed-depth tree with about 100 nodes having cursors: 1.4 seconds\n- One 2k-node mixed-depth tree with cursors and onChange listeners  on every node: 1.2 seconds\n- Making one tree all at once from a giant object is basically instant\n- For comparison, making a 100k-value plain object took 0.04 seconds and 100k function instantiations + calling took 0.04 seconds.\n\n## Contribution\n\nPull 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\n\n## Full API\n\n```ts\nclass Dentata\u003cT\u003e {\n    constructor(data: T);\n    // Get the current value at the cursor\n    get(): DeepReadonly\u003cT\u003e;\n    // Set data of current cursor and notify relevant onChange listeners. Set to `undefined` to remove all listeners and descendant cursors.\n    set(newVal: T): void;\n    // Set value at key\n    setIn\u003cK extends keyof T\u003e(k: K, val: T[K]): void;\n    // Alias for get + set. Update the old value into a new value. Do not mutate the argument.\n    apply(update: (prev: DeepReadonly\u003cT\u003e) =\u003e T): void;\n    // 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).\n    select\u003cK extends keyof T\u003e(key: K): Dentata\u003cT[K]\u003e;\n    // Alias for Dentata.select\n    s\u003cK extends keyof T\u003e(key: K): Dentata\u003cT[K]\u003e;\n    // Listen for changes to the data at this cursor, including changes originating in parents or children.\n    onChange(handleChange: Listener\u003cT\u003e): void;\n    // Remove all onChange listeners on this cursor\n    clearListeners(): void;\n}\n\n// An onChange callback\ntype Listener\u003cT\u003e = (newVal: DeepReadonly\u003cT\u003e, oldVal: DeepReadonly\u003cT\u003e) =\u003e void;\n\n// Alias for Dentata\nconst Dent: typeof Dentata;\ntype Dent\u003cT\u003e = Dentata\u003cT\u003e;\n\n// Return type of syntheticCursor\ninterface DentataLike\u003cT\u003e {\n    get: () =\u003e DeepReadonly\u003cT\u003e;\n    onChange: (l: Listener\u003cT\u003e) =\u003e void;\n}\n\n// Create a synthetic data cursor for computed values on another data cursor\nfunction syntheticCursor\u003cInputData, OutputData\u003e(\n    fromCursor: DentataLike\u003cInputData\u003e,\n    compute: (t: DeepReadonly\u003cInputData\u003e) =\u003e OutputData,\n    settings?: { equality: \"===\" | \"deep\"; }\n    ): DentataLike\u003cOutputData\u003e;\n\n// The equality algorithm, mainly exported so you can test it for your particular case\nfunction deepEquals (a: unknown, b: unknown) =\u003e boolean;\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqpwo%2Fdentata","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqpwo%2Fdentata","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqpwo%2Fdentata/lists"}