npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

use-tree-state

v1.1.1

Published

A super-light and customizable React hook to manage tree state like never before

Downloads

18,613

Readme

Use Tree State

travis build codecov npm bundle size dependency
npm npm GitHub top language

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

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

$ yarn add use-tree-state
$ npm install use-tree-state --save

🌀 initialization

import useTreeState, { testData } from 'use-tree-state';

const TreeApp = () => {
  const { treeState } = useTreeState({ data: testData });

  return (<Tree state={ treeState } />);
};

🌀 custom initialization

Initial tree state is an object that describes a nested tree node structure, which looks like:

{
  // 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:

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<int>, ...args) => update state internally
or
fByProp(propName: string, targetValue: any, ...args) => update state internally

For more details please refer to Built-in Reducers section.

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.

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<int>, ...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

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<int>, 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<int>, 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<int>, newName: string)

renameNodeByProp(propName: string, targetValue: any, newName: string)

You know what it is.

deleteNode(path: array<int>)

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<int>, 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 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<int>) .

🌀 method 1: wrap custom reducers (recommended)

We can build any custom reducers of format

myReducer(root: tree-state-obj, path: array<int> | 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<int> | null, ...params)

to update the treeState.

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 (<>
    <button onClick={ renameFirstNodeToPikaPikaPika }>
      pika pika
    </button>

    <Tree state={ treeState } />
  </>);

};

🌀 method 2: set tree state from outside

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 (<>
    <button onClick={ renameFirstNodeToPikaPikaPika }>
      pika pika
    </button>

    <Tree state={ treeState } />
  </>);
};

🌀 find node by any node property

⚡️live exmaple

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<int>
  • findAllTargetPathByProp(root: tree-state-obj, propName: string, targetValue: string): array<array<int>>

For example, let's rewrite renameNodeByProp in a more custom way

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 <Tree /> component, which perfectly matches such interface (example)

Bugs? Questions? Contributions?

Feel free to open an issue, or create a pull request!