https://github.com/shunjizhan/use-tree-state
A super-light and customizable React hook to manage tree state like never before
https://github.com/shunjizhan/use-tree-state
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
Last synced: 5 months ago
JSON representation
A super-light and customizable React hook to manage tree state like never before
- Host: GitHub
- URL: https://github.com/shunjizhan/use-tree-state
- Owner: shunjizhan
- Created: 2021-05-26T09:01:18.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2021-12-13T13:58:09.000Z (over 3 years ago)
- Last Synced: 2024-10-30T01:56:44.873Z (6 months ago)
- 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
- Language: JavaScript
- Homepage: https://www.npmjs.com/package/use-tree-state
- Size: 385 KB
- Stars: 4
- Watchers: 1
- Forks: 4
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Use Tree State
[](https://travis-ci.com/shunjizhan/use-tree-state) [](https://codecov.io/gh/shunjizhan/use-tree-state)  
[](https://www.npmjs.com/package/use-tree-state) [](https://www.npmjs.com/package/use-tree-state) A super-light and customizable React hook to manage tree state like never before ✨✨
An example package that uses this hook internally: [react-folder-tree](https://www.npmjs.com/package/react-folder-tree)
## Features
✅ **built in CRUD handlers**: `add`, `modify`, `delete` tree nodes with 1 line of code
✅ **custom handlers**: define any custom state transition for your need
✅ **half check**: auto calculate new `checked` status for all nodes
✅ **onChange**: listen to state change and events## Usage
### 🌀 install
```bash
$ yarn add use-tree-state
$ npm install use-tree-state --save
```### 🌀 initialization
```ts
import useTreeState, { testData } from 'use-tree-state';const TreeApp = () => {
const { treeState } = useTreeState({ data: testData });return ();
};
```### 🌀 custom initialization
Initial tree state is an object that describes a nested tree node structure, which looks like:
```jsx
{
// reserved keys, can customize initial value
name: 'root node',
checked (optional): 0 (unchecked, default) | 0.5 (half checked) | 1(checked),
isOpen (optional): true (default) | false,
children (optional): [array of treenode],// internal key (auto generated), plz don't include it in the initial data
_id: 0,// all other keys are not reserved, can carry any extra info about this node
nickname (optional): 'pikachu',
url (optional): 'www.pokemon.com',
...
}
```
`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'`.Example:
```ts
const { treeState } = useTreeState({
data: testData,
options: {
initCheckedStatus: 'checked', // 'unchecked' (default) | 'checked' | 'custom'
initOpenStatus: 'open', // 'open' (default) | 'closed' | 'custom'
}
});
```### 🌀 update tree state
There are a couple built in tree state reducers that can update tree state conveniently.Note that these `reducers` are slightly different than `redux reducers`. These are more like `wrapped reducers` which are functions that
`f(path: array, ...args) => update state internally`
or
`fByProp(propName: string, targetValue: any, ...args) => update state internally`For more details please refer to [Built-in Reducers](#built-in-reducers) section.
```ts
const TreeApp = () => {
const { treeState, reducers } = useTreeState({ data: testData });
const {
// update state using node's path to find target
checkNode,
toggleOpen,
renameNode,
deleteNode,
addNode,// update state using any node's property to find target
checkNodeByProp,
toggleOpenByProp,
renameNodeByProp,
deleteNodeByProp,
addNodeByProp,
} = reducers;const check_first_node = () => checkNode([0]);
const check_node_whos_name_is_Goku = () => checkNodeByProp('name', 'Goku');const open_first_node = () => toggleOpen([0], 1);
const open_node_whos_url_is_www = () => toggleOpenByProp('url', 'www', 1);
const close_node_whos_num_is_123 = () => toggleOpenByProp('num', 123, 0);const rename_third_node_to_pikachu = () => renameNode([2], 'pikachu');
const rename_snorlax_node_to_pikachu = () => renameNode('name', 'snorlax', 'pikachu');const remove_fourth_node = () => deleteNode([3]);
const remove_unnecessary_node = () => deleteNodeByProp('necessary', false);const add_leaf_node_in_root_node = () => addNode([], false);
const add_parent_node_in_Pokemon_node = () => addNodeByProp('type', 'Pokemon', true);return (...);
};
```### 🌀 onChange listener
we can pass in an `onChange(newState: tree-state-obj, event: obj)` to the hook to listen for state change event.
```ts
const handleStateChange = (newState, event) => {
const { type, path, params } = event;console.log('last event: ', { type, path, params });
console.log('state changed to: ', newState);
};const { treeState } = useTreeState({
data: testData,
onChange: handleStateChange, // <== here!!
});
```## Built-in Reducers
There 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.#### 1) find target node by path
- `reducers.checkNode`
- `reducers.toggleOpen`
- `reducers.renameNode`
- `reducers.deleteNode`
- `reducers.addNode`their format is `f(path: array, ...args) => update state internally`, where `path` is an array of indexes from root to the target node.
An example that shows each node and corresponding path
```ts
const treeState = {
name: 'root', // path = []
children: [
{ name: 'node_0' } // path = [0]
{ name: 'node_1' } // path = [1]
{
name: 'node_2', // path = [2]
children: [
{ name: 'node_2_0' }, // path = [2, 0]
{ name: 'node_2_1' }, // path = [2, 1]
],
}
],
};
```#### 2) find target node by property (can be any property in tree node data)
- `reducers.checkNodeByProp`
- `reducers.toggleOpenByProp`
- `reducers.renameNodeByProp`
- `reducers.deleteNodeByProp`
- `reducers.addNodeByProp`their format is `fByProp(propName: string, targetValue: any, ...args) => update state internally`
### 🌀 reducers details
#### • `checkNode(path: array, checked: 1 | 0)`
#### • `checkNodeByProp(propName: string, targetValue: any, checked: 1 | 0)`
Set `checked` property of the target node, `1` for 'checked', `0` for 'unchecked'.It will also update checked status for all other nodes:
- if we (un)checked a parent node, all children nodes will also be (un)checked
- if some (but not all) of a node's children are checked, this node becomes half check (internally set `checked` = 0.5)
#### • `toggleOpen(path: array, isOpen: bool)`
#### • `toggleOpenByProp(propName: string, targetValue: any, isOpen: bool)`
Set the open status `isOpen` for the target node. `isOpen: false` usually means in UI we shouldn't see it's children.**This only works for parent nodes**, which are the nodes that has `children` property.
#### • `renameNode(path: array, newName: string)`
#### • `renameNodeByProp(propName: string, targetValue: any, newName: string)`
You know what it is.
#### • `deleteNode(path: array)`
#### • `deleteNodeByProp(propName: string, targetValue: any)`
Delete the target node. If target node is a parent, all of it's children will also be removed.
#### • `addNode(path: array, hasChildren: bool)`
#### • `addNodeByProp(propName: string, targetValue: any, hasChildren: bool)`
Add a node as a children of target node. `hasChildren: true` means this new node is a parent node, otherwise it is a leaf node.**This only works for parent nodes**.
#### • `setTreeState(newState: tree-state-object)`
Instead 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 : )## Custom Reducers
There 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)` .### 🌀 method 1: wrap custom reducers (recommended)
We can build any custom reducers of format`myReducer(root: tree-state-obj, path: array | null, ...params): tree-state-obj`
and pass it to the hook constructor. Hook will then expose a wrapped version of it, and we can use it like
`reducers.myReducer(path: array | null, ...params)`
to update the treeState.
```ts
import useTreeState, {
testData,
findTargetNode,
} from 'use-tree-state';// this app demos how to build a custom reducer that rename a node to 'pikachu'
const TreeApp = () => {
// our custom reducer
const renameToPikachuNTimes = (root, path, n) => {
const targetNode = findTargetNode(root, path);
targetNode.name = 'pika'.repeat(n);return { ...root };
};const { treeState, reducers } = useTreeState({
data: testData,
customReducers: {
renameToPikachuNTimes, // pass in and hook will wrap it
},
});const renameFirstNodeToPikaPikaPika = () => {
// use the wrapped custom reducer
reducers.renameToPikachuNTimes([0], 3);
}return (<>
pika pika
>);};
```### 🌀 method 2: set tree state from outside
```ts
const TreeApp = () => {
const { treeState, reducers } = useTreeState({ data: testData });
const { setTreeState } = reducers;// our custom reducer to set tree state directly
const renameToPikachuNTimes = (root, path, n) => {
// treeState is a ref to the internal state, plz don't alter it directly
const newState = deepClone(root);const targetNode = findTargetNode(newState, path);
targetNode.name = 'pika'.repeat(n);setTreeState(newState);
};const renameFirstNodeToPikaPikaPika = () => {
renameToPikachuNTimes(treeState, [0], 3);
}return (<>
pika pika
>);
};
```### 🌀 find node by any node property
[⚡️live exmaple](https://codesandbox.io/s/react-playground-forked-55bt9?file=/index.js)Other 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:
- `findTargetPathByProp(root: tree-state-obj, propName: string, targetValue: string): array`
- `findAllTargetPathByProp(root: tree-state-obj, propName: string, targetValue: string): array>`For example, let's rewrite `renameNodeByProp` in a more custom way
```ts
import { findTargetPathByProp } from 'use-tree-state';// our custom reducer, note that we omit the `path` param as _ since we don't need it
const renameNodeByTargetName = (root, _, targetName, newName) => {
// only need this one extra line to find path first
// if 'name' is not unique, we can find all nodes by `findAllTargetPathByProp`
const path = findTargetPathByProp(root, 'name', targetName); // <== here!!!// then everything else is just the same
const targetNode = findTargetNode(root, path);
targetNode.name = newName;return { ...root };
};// ......
// then we can use it like
reducers.renameNodeByTargetName(null, 'snorlax', 'pikachu');
```**Side Notes**
We chose to use `path` to find target node as the primary interface because:
- path is always unique
- this is the fastest way to find a target node
- we can dynamically general path in `` component, which perfectly matches such interface ([example](https://github.com/shunjizhan/react-folder-tree/blob/master/src/components/TreeNode/TreeNode.jsx#L30))## Bugs? Questions? Contributions?
Feel free to [open an issue](https://github.com/shunjizhan/use-tree-state/issues), or create a pull request!