react-arborist

repository·main·Indexed 25 days ago

https://github.com/jameskerr/react-arborist

A complete solution for building complex tree views in React, similar to VSCode sidebars or file explorers. It supports virtualization, drag-and-drop, keyboard navigation, and both controlled and uncontrolled modes. Features include custom node rendering, search filtering with searchTerm and searchMatch, and a comprehensive NodeApi for managing selection, expansion, and editing states.

Tokens
11.5K
Snippets
32
Records
68
Agent score
86%

What's inside react-arborist

  1. Understand react-arborist domain models

    main

    To work effectively with react-arborist, it is helpful to understand its core domain models:

    • Tree View: The main component that renders the UI.
    • Source Data: The raw data you provide to the library.
    • Node Object: The interface expected by the TreeManager and TreeController.
    • Source Data Proxy: A wrapper around your Source Data that conforms to the NodeObject interface and includes methods for mutating the underlying data.
    • Tree Manager: Responsible for responding to change events from the TreeView.
    • Tree Controller: The programming API used to interact with the tree.
    • Node Controller: The programming API used to interact with a specific node.
    • Partial Controller: An object containing value and onChange properties, used to manage slices of component state.
  2. Filter the tree with searchTerm and searchMatch

    main

    Use the searchTerm prop to filter nodes. If a child matches, all its parents are automatically shown to preserve structure. By default, the search is a loose JSON-stringified match on the node's data. To match specific fields, provide a custom searchMatch function.

    function App() {
      const term = useSearchTermString()
      return (
        <Tree
          data={data}
          searchTerm={term}
          searchMatch={
            (node, term) => node.data.name.toLowerCase().includes(term.toLowerCase())
          }
        />
      )
    }
  3. Control the Tree data with controlled props

    main

    To make the tree a controlled component, use the data prop instead of initialData. You must then implement handlers for all data modifications using the following props:

    • onCreate: Called when a new node is created. Receives { parentId, index, type }.
    • onRename: Called when a node is renamed. Receives { id, name }.
    • onMove: Called when a node is moved. Receives { dragIds, parentId, index }.
    • onDelete: Called when nodes are deleted. Receives { ids }.
    function App() {
      /* Handle the data modifications outside the tree component */
      const onCreate = ({ parentId, index, type }) => {};
      const onRename = ({ id, name }) => {};
      const onMove = ({ dragIds, parentId, index }) => {};
      const onDelete = ({ ids }) => {};
    
      return (
        <Tree
          data={data}
          onCreate={onCreate}
          onRename={onRename}
          onMove={onMove}
          onDelete={onDelete}
        />
      );
    }
  4. Customize Tree appearance and Node rendering

    main

    You can customize the tree's dimensions and layout by providing a custom Node component as a child of the Tree component. Use props like width, height, indent, rowHeight, overscanCount, paddingTop, paddingBottom, and padding to control the layout.

    function App() {
      return (
        <Tree
          initialData={data}
          openByDefault={false}
          width={600}
          height={1000}
          indent={24}
          rowHeight={36}
          overscanCount={1}
          paddingTop={30}
          paddingBottom={10}
          padding={25 /* sets both */}
        >
          {Node}
        </Tree>
      );
    }
    
    function Node({ node, style, dragHandle }) {
      /* This node instance can do many things. See the API reference. */
      return (
        <div style={style} ref={dragHandle}>
          {node.isLeaf ? "🍁" : "🗀"}
          {node.data.name}
        </div>
      );
    }
  5. Control expanded state with useOpens

    main

    To persist or control the expanded/collapsed state of the tree, use the opens prop. You can manage this state using the useOpens hook, which can either extract state from your data or manage an external object.

    // Option 1: Extract from tree data
    const [opens, setOpens] = useOpens(data, {
      id: (d) => d.path,
      isOpen: (d) => d.isOpen
    });
    
    // Option 2: Use an external state object
    const [opens, setOpens] = useState({});
    
    <Tree
      opens={{
        value: opens,
        onChange: (newValue) => setOpens(newValue)
      }}
    />
  6. Run tests for react-arborist

    main

    To run the Jest tests for the react-arborist package, navigate to the package directory and run yarn test.

    Useful test files for reference:

    • src/interfaces/tree-api.test.ts: Pure API behavior.
    • src/components/provider.test.tsx: Rendered tree behavior.
    • src/dnd/drag-hook.test.ts: Drag-and-drop behavior.
    cd modules/react-arborist
    yarn test
  7. Access the Tree API via ref

    main

    You can access the Tree API instance (to call methods like tree.selectAll()) by passing a ref to the Tree component.

    function App() {
      const treeRef = useRef();
    
      useEffect(() => {
        const tree = treeRef.current;
        tree.selectAll();
        /* See the Tree API reference for all you can do with it. */
      }, []);
    
      return <Tree initialData={data} ref={treeRef} />;
    }
  8. Sync selection and scrolling

    main

    To programmatically select and scroll to a specific node, pass its ID to the selection prop. Whenever the value of selection changes, the tree will automatically select and scroll to that node.

    function App() {
      const chatId = useCurrentChatId();
    
      /* 
        Whenever the currentChatRoomId changes, 
        the tree will automatically select it and scroll to it. 
      */
    
      return <Tree initialData={data} selection={chatId} />;
    }
  9. Drag nodes to external drop targets

    main

    To drag nodes from the tree to an external component, ensure both the tree and the target share the same react-dnd backend by wrapping them in a single DndProvider and passing the manager to the tree via the dndManager prop.

    The dragged item carries the node's data. By default, rows use the "NODE" item type. If you change the tree's dragType prop, the tree's internal drop targets will no longer accept those nodes (making them non-reorderable within the tree).

    const [, drop] = useDrop(() => ({
      accept: "NODE",
      drop: (item) => console.log(item.data), // the dragged node's data
    }));
  10. Manage selection state with useMultiSelection

    main

    To handle selection (including multi-selection) and allow external control, use the selection prop. Using hooks like useMultiSelection allows you to sync selection with external IDs or state managers.

    const id = useSelector(Current.fileId)
    const selection = useMultiSelection(id)
    
    useEffect(() => {
      selection.only(id)
    }, [id])
    
    <Tree
      selection={{
        value: selection.value,
        onChange: selection.set
      }}
    />
  11. Initialize a TreeManager with source data

    main

    Instead of manually converting your source data into NodeObject instances, you can use the createTreeManager(sourceData, options) helper function.

    This returns a TreeManager instance. The TreeManager.nodes property returns an array of SourceDataProxy objects. These proxies conform to the NodeObject interface but also include methods to mutate the original source data. The TreeManager also provides methods to mutate source data in response to tree change events.