{"id":22375617,"url":"https://github.com/shunjizhan/use-tree-state","last_synced_at":"2025-07-30T22:32:20.242Z","repository":{"id":52233444,"uuid":"370970527","full_name":"shunjizhan/use-tree-state","owner":"shunjizhan","description":"A super-light and customizable React hook to manage tree state like never before","archived":false,"fork":false,"pushed_at":"2021-12-13T13:58:09.000Z","size":394,"stargazers_count":4,"open_issues_count":4,"forks_count":4,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-30T01:56:44.873Z","etag":null,"topics":["custom-hook","folder-structure","folder-tooling","folder-viewer","hook","npm-package","react","react-hooks","react-state","react-state-hook","react-state-management","recursion","recursion-tree","state-management","tree-node","tree-state","tree-structure","treeview","treeview-component","treeview-control"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/use-tree-state","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shunjizhan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-05-26T09:01:18.000Z","updated_at":"2024-02-28T07:03:31.000Z","dependencies_parsed_at":"2022-09-11T02:12:04.907Z","dependency_job_id":null,"html_url":"https://github.com/shunjizhan/use-tree-state","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shunjizhan%2Fuse-tree-state","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shunjizhan%2Fuse-tree-state/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shunjizhan%2Fuse-tree-state/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shunjizhan%2Fuse-tree-state/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shunjizhan","download_url":"https://codeload.github.com/shunjizhan/use-tree-state/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228194518,"owners_count":17883129,"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","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":["custom-hook","folder-structure","folder-tooling","folder-viewer","hook","npm-package","react","react-hooks","react-state","react-state-hook","react-state-management","recursion","recursion-tree","state-management","tree-node","tree-state","tree-structure","treeview","treeview-component","treeview-control"],"created_at":"2024-12-04T21:27:01.442Z","updated_at":"2024-12-04T21:27:02.146Z","avatar_url":"https://github.com/shunjizhan.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Use Tree State\n[![travis build](https://img.shields.io/travis/com/shunjizhan/use-tree-state?logo=travis)](https://travis-ci.com/shunjizhan/use-tree-state) [![codecov](https://codecov.io/gh/shunjizhan/use-tree-state/branch/main/graph/badge.svg?token=R15MSCTFHN)](https://codecov.io/gh/shunjizhan/use-tree-state) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/use-tree-state?color=light%20green\u0026label=only\u0026logo=webpack) ![dependency](https://img.shields.io/badge/dependency-zero-brightgreen?\u0026logo=git)  \n[![npm](https://img.shields.io/npm/v/use-tree-state?logo=npm)](https://www.npmjs.com/package/use-tree-state) [![npm](https://img.shields.io/npm/dm/use-tree-state?logo=DocuSign\u0026color=blue)](https://www.npmjs.com/package/use-tree-state) ![GitHub top language](https://img.shields.io/github/languages/top/shunjizhan/use-tree-state?logo=react)\n\nA super-light and customizable React hook to manage tree state like never before ✨✨\n\nAn example package that uses this hook internally: [react-folder-tree](https://www.npmjs.com/package/react-folder-tree)\n## Features\n✅ **built in CRUD handlers**: `add`, `modify`, `delete` tree nodes with 1 line of code  \n✅ **custom handlers**: define any custom state transition for your need  \n✅ **half check**: auto calculate new `checked` status for all nodes  \n✅ **onChange**: listen to state change and events  \n\n## Usage\n### 🌀 install\n```bash\n$ yarn add use-tree-state\n$ npm install use-tree-state --save\n```\n\n### 🌀 initialization\n```ts\nimport useTreeState, { testData } from 'use-tree-state';\n\nconst TreeApp = () =\u003e {\n  const { treeState } = useTreeState({ data: testData });\n\n  return (\u003cTree state={ treeState } /\u003e);\n};\n```\n\n### 🌀 custom initialization\nInitial tree state is an object that describes a nested tree node structure, which looks like:\n```jsx\n{\n  // reserved keys, can customize initial value\n  name: 'root node',  \n  checked (optional): 0 (unchecked, default) | 0.5 (half checked) | 1(checked),\n  isOpen (optional): true (default) | false,\n  children (optional): [array of treenode],\n\n  // internal key (auto generated), plz don't include it in the initial data\n  _id: 0,\n\n  // all other keys are not reserved, can carry any extra info about this node\n  nickname (optional): 'pikachu',\n  url (optional): 'www.pokemon.com',\n  ...\n}\n```\n`checked` and `isOpen` status could be auto initialized by props `initCheckedStatus` and `initOpenStatus`. We can also provide data with custom `checked` and `isOpen` status, and set `initCheckedStatus` and `initOpenStatus` to `'custom'`.\n\nExample:\n```ts\nconst { treeState } = useTreeState({\n  data: testData,\n  options: {\n    initCheckedStatus: 'checked',   // 'unchecked' (default) | 'checked' | 'custom'\n    initOpenStatus: 'open',         // 'open' (default) | 'closed' | 'custom'\n  }\n});\n```\n\n### 🌀 update tree state\nThere are a couple built in tree state reducers that can update tree state conveniently.\n\nNote that these `reducers` are slightly different than `redux reducers`. These are more like  `wrapped reducers` which are functions that\n\n`f(path: array\u003cint\u003e, ...args) =\u003e update state internally`  \nor   \n`fByProp(propName: string, targetValue: any, ...args) =\u003e update state internally`\n\nFor more details please refer to [Built-in Reducers](#built-in-reducers) section.\n```ts\nconst TreeApp = () =\u003e {\n  const { treeState, reducers } = useTreeState({ data: testData });\n  const {\n    // update state using node's path to find target\n    checkNode,\n    toggleOpen,\n    renameNode,\n    deleteNode,\n    addNode,\n\n    // update state using any node's property to find target\n    checkNodeByProp,\n    toggleOpenByProp,\n    renameNodeByProp,\n    deleteNodeByProp,\n    addNodeByProp,\n  } = reducers;\n\n  const check_first_node = () =\u003e checkNode([0]);\n  const check_node_whos_name_is_Goku = () =\u003e checkNodeByProp('name', 'Goku');\n\n  const open_first_node = () =\u003e toggleOpen([0], 1);\n  const open_node_whos_url_is_www = () =\u003e toggleOpenByProp('url', 'www', 1);\n  const close_node_whos_num_is_123 = () =\u003e toggleOpenByProp('num', 123, 0);\n\n  const rename_third_node_to_pikachu = () =\u003e renameNode([2], 'pikachu');\n  const rename_snorlax_node_to_pikachu = () =\u003e renameNode('name', 'snorlax', 'pikachu');\n\n  const remove_fourth_node = () =\u003e deleteNode([3]);\n  const remove_unnecessary_node = () =\u003e deleteNodeByProp('necessary', false);\n\n  const add_leaf_node_in_root_node = () =\u003e addNode([], false);\n  const add_parent_node_in_Pokemon_node = () =\u003e addNodeByProp('type', 'Pokemon', true);\n\n  return (...);\n};\n```\n\n### 🌀 onChange listener\nwe can pass in an `onChange(newState: tree-state-obj, event: obj)` to the hook to listen for state change event.\n```ts\nconst handleStateChange = (newState, event) =\u003e {\n  const { type, path, params } = event;\n\n  console.log('last event: ', { type, path, params });\n  console.log('state changed to: ', newState);\n};\n\nconst { treeState } = useTreeState({\n  data: testData,\n  onChange: handleStateChange,      // \u003c== here!!\n});\n```\n\n\n## Built-in Reducers\nThere are two types of built in reducers (or call it handlers if you prefer) that differ in how they find target node to operate on.\n\n#### 1) find target node by path\n- `reducers.checkNode`\n- `reducers.toggleOpen`\n- `reducers.renameNode`\n- `reducers.deleteNode`\n- `reducers.addNode`\n\ntheir format is `f(path: array\u003cint\u003e, ...args) =\u003e update state internally`, where `path` is an array of indexes from root to the target node.\n\nAn example that shows each node and corresponding path\n```ts\nconst treeState = {\n  name: 'root',         // path = []\n  children: [\n    { name: 'node_0' }    // path = [0]\n    { name: 'node_1' }    // path = [1]\n    {\n      name: 'node_2',     // path = [2]\n      children: [\n        { name: 'node_2_0' },   // path = [2, 0]\n        { name: 'node_2_1' },   // path = [2, 1]\n      ],\n    }\n  ],\n};\n```\n\n#### 2) find target node by property (can be any property in tree node data)\n- `reducers.checkNodeByProp`\n- `reducers.toggleOpenByProp`\n- `reducers.renameNodeByProp`\n- `reducers.deleteNodeByProp`\n- `reducers.addNodeByProp`\n\ntheir format is `fByProp(propName: string, targetValue: any, ...args) =\u003e update state internally`\n\n\n### 🌀 reducers details\n\n#### • `checkNode(path: array\u003cint\u003e, checked: 1 | 0)`\n#### • `checkNodeByProp(propName: string, targetValue: any, checked: 1 | 0)`\nSet `checked` property of the target node, `1` for 'checked', `0` for 'unchecked'.\n\nIt will also update checked status for all other nodes:\n- if we (un)checked a parent node, all children nodes will also be (un)checked\n- if some (but not all) of a node's children are checked, this node becomes half check (internally set `checked` = 0.5)\n\n\u003cbr\u003e\n\n#### • `toggleOpen(path: array\u003cint\u003e, isOpen: bool)`\n#### • `toggleOpenByProp(propName: string, targetValue: any, isOpen: bool)`\nSet the open status `isOpen` for the target node. `isOpen: false` usually means in UI we shouldn't see it's children.\n\n**This only works for parent nodes**, which are the nodes that has `children` property.\n\n\u003cbr\u003e\n\n#### • `renameNode(path: array\u003cint\u003e, newName: string)`\n#### • `renameNodeByProp(propName: string, targetValue: any, newName: string)`\nYou know what it is.\n\n\u003cbr\u003e\n\n#### • `deleteNode(path: array\u003cint\u003e)`\n#### • `deleteNodeByProp(propName: string, targetValue: any)`\nDelete the target node. If target node is a parent, all of it's children will also be removed.\n\n\u003cbr\u003e\n\n#### • `addNode(path: array\u003cint\u003e, hasChildren: bool)`\n#### • `addNodeByProp(propName: string, targetValue: any, hasChildren: bool)`\nAdd a node as a children of target node. `hasChildren: true` means this new node is a parent node, otherwise it is a leaf node.\n\n**This only works for parent nodes**.\n\n\u003cbr\u003e\n\n#### • `setTreeState(newState: tree-state-object)`\nInstead of 'update' the tree state, this will set whole tree state directly. Didn't test this method, but leave this api anyways, so use with cautions! And plz [open an issue](https://github.com/shunjizhan/use-tree-state/issues) if it doesn't work : )\n\n## Custom Reducers\nThere are two ways to build custom state transition functions. We provide an util to help find the target node:  `findTargetNode(root: tree-state-obj, path: array\u003cint\u003e)` .\n\n### 🌀 method 1: wrap custom reducers (recommended)\nWe can build any custom reducers of format\n\n`myReducer(root: tree-state-obj, path: array\u003cint\u003e | null, ...params): tree-state-obj`\n\nand pass it to the hook constructor. Hook will then expose a wrapped version of it, and we can use it like\n\n`reducers.myReducer(path: array\u003cint\u003e | null, ...params)` \n\nto update the treeState. \n```ts\nimport useTreeState, {\n  testData,\n  findTargetNode,\n} from 'use-tree-state';\n\n// this app demos how to build a custom reducer that rename a node to 'pikachu'\nconst TreeApp = () =\u003e {\n  // our custom reducer\n  const renameToPikachuNTimes = (root, path, n) =\u003e {\n    const targetNode = findTargetNode(root, path);\n    targetNode.name = 'pika'.repeat(n);\n\n    return { ...root };\n  };\n\n  const { treeState, reducers } = useTreeState({\n    data: testData,\n    customReducers: {\n      renameToPikachuNTimes,  // pass in and hook will wrap it\n    },\n  });\n\n  const renameFirstNodeToPikaPikaPika = () =\u003e {\n    // use the wrapped custom reducer\n    reducers.renameToPikachuNTimes([0], 3);\n  }\n\n  return (\u003c\u003e\n    \u003cbutton onClick={ renameFirstNodeToPikaPikaPika }\u003e\n      pika pika\n    \u003c/button\u003e\n\n    \u003cTree state={ treeState } /\u003e\n  \u003c/\u003e);\n\n};\n```\n\n### 🌀 method 2: set tree state from outside\n```ts\nconst TreeApp = () =\u003e {\n  const { treeState, reducers } = useTreeState({ data: testData });\n  const { setTreeState } = reducers;\n\n  // our custom reducer to set tree state directly\n  const renameToPikachuNTimes = (root, path, n) =\u003e {\n    // treeState is a ref to the internal state, plz don't alter it directly\n    const newState = deepClone(root); \n\n    const targetNode = findTargetNode(newState, path);\n    targetNode.name = 'pika'.repeat(n);\n\n    setTreeState(newState);\n  };\n\n  const renameFirstNodeToPikaPikaPika = () =\u003e {\n    renameToPikachuNTimes(treeState, [0], 3);\n  }\n\n  return (\u003c\u003e\n    \u003cbutton onClick={ renameFirstNodeToPikaPikaPika }\u003e\n      pika pika\n    \u003c/button\u003e\n\n    \u003cTree state={ treeState } /\u003e\n  \u003c/\u003e);\n};\n```\n\n### 🌀 find node by any node property\n[⚡️live exmaple](https://codesandbox.io/s/react-playground-forked-55bt9?file=/index.js)\n\nOther than the built-in reducers that **CRUD by prop**, we can build more general reducers that **do anything by prop**, with the help of these two adapters:\n- `findTargetPathByProp(root: tree-state-obj, propName: string, targetValue: string): array\u003cint\u003e`  \n- `findAllTargetPathByProp(root: tree-state-obj, propName: string, targetValue: string): array\u003carray\u003cint\u003e\u003e` \n\nFor example, let's rewrite `renameNodeByProp` in a more custom way\n```ts\nimport { findTargetPathByProp } from 'use-tree-state';\n\n// our custom reducer, note that we omit the `path` param as _ since we don't need it\nconst renameNodeByTargetName = (root, _, targetName, newName) =\u003e {\n  // only need this one extra line to find path first\n  // if 'name' is not unique, we can find all nodes by `findAllTargetPathByProp`\n  const path = findTargetPathByProp(root, 'name', targetName);    // \u003c== here!!!\n\n  // then everything else is just the same\n  const targetNode = findTargetNode(root, path);\n  targetNode.name = newName;\n\n  return { ...root };\n};\n\n// ......\n\n// then we can use it like\nreducers.renameNodeByTargetName(null, 'snorlax', 'pikachu');\n```\n\n**Side Notes**\nWe chose to use `path` to find target node as the primary interface because:\n- path is always unique\n- this is the fastest way to find a target node\n- we can dynamically general path in `\u003cTree /\u003e` component, which perfectly matches such interface ([example](https://github.com/shunjizhan/react-folder-tree/blob/master/src/components/TreeNode/TreeNode.jsx#L30))\n\n## Bugs? Questions? Contributions?\nFeel free to [open an issue](https://github.com/shunjizhan/use-tree-state/issues), or create a pull request!","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshunjizhan%2Fuse-tree-state","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshunjizhan%2Fuse-tree-state","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshunjizhan%2Fuse-tree-state/lists"}