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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@node-tree/state

v0.0.1

Published

Reactive state management for virtual file systems

Readme

@node-tree/state

Reactive state management for virtual file systems. This package provides a complete tree state solution that mirrors any VFS implementation from @firesystem/core, with built-in support for navigation, selection, and expansion states.

Features

  • 🎯 VFS Mirroring - Automatically syncs with any firesystem implementation
  • 🚀 Framework Agnostic - Works with Zustand, MobX, or vanilla JS
  • 📁 Tree Operations - Navigation, selection, expansion with keyboard-friendly APIs
  • 👀 Real-time Sync - Auto-updates when files change via watch system
  • 🔍 Smart Queries - Efficient getters for visible nodes, children, parents
  • Fully Typed - Complete TypeScript support
  • 🧪 Well Tested - Comprehensive unit and integration tests

Installation

npm install @node-tree/state @firesystem/core
# or
pnpm add @node-tree/state @firesystem/core
# or
yarn add @node-tree/state @firesystem/core

Choose your preferred state management library:

# For Zustand (recommended)
npm install zustand

# For MobX
npm install mobx mobx-react-lite

Quick Start

With Zustand (Recommended)

import { createZustandTreeStore } from "@node-tree/state";
import { MemoryFileSystem } from "@firesystem/memory";

// Create store with optional file system
const fs = new MemoryFileSystem();
const useTreeStore = createZustandTreeStore(fs);

// Use in React component
function FileExplorer() {
  const { 
    visibleNodes, 
    cursor, 
    expandNode, 
    collapseNode,
    moveCursorDown,
    moveCursorUp 
  } = useTreeStore();
  
  const nodes = useTreeStore(state => state.getVisibleNodes());
  
  return (
    <div>
      {nodes.map(node => (
        <div 
          key={node.id}
          style={{ paddingLeft: node.level * 20 }}
          className={cursor === node.id ? 'selected' : ''}
        >
          {node.type === 'directory' && (
            <button onClick={() => toggleNode(node.id)}>
              {isExpanded(node.id) ? '▼' : '▶'}
            </button>
          )}
          {node.name}
        </div>
      ))}
    </div>
  );
}

With MobX

import { createMobXTreeStore } from "@node-tree/state";
import { observer } from "mobx-react-lite";

const treeStore = createMobXTreeStore(fs);

const FileExplorer = observer(() => {
  const nodes = treeStore.getVisibleNodes();
  
  return (
    <div>
      {nodes.map(node => (
        <div key={node.id}>
          {node.name}
        </div>
      ))}
    </div>
  );
});

Vanilla JavaScript

import { TreeStateCore } from "@node-tree/state";

const treeState = new TreeStateCore();

// Load from file entries
treeState.loadFromFs([
  { path: "/src", name: "src", type: "directory" },
  { path: "/src/index.ts", name: "index.ts", type: "file" }
]);

// Navigate
treeState.moveCursorDown();
treeState.expandNode("src");

// Query
const visibleNodes = treeState.getVisibleNodes();

Core Concepts

Tree Node Structure

interface TreeNode {
  id: string;              // Unique identifier
  path: string;            // Full path
  name: string;            // File/directory name
  type: "file" | "directory";
  level: number;           // Depth in tree
  parentId?: string;       // Parent node ID
  expanded?: boolean;      // For directories
  // ... other FileEntry properties
}

Navigation Actions

// Cursor movement
store.moveCursorUp();
store.moveCursorDown();
store.moveCursorToFirst();
store.moveCursorToLast();
store.setCursor(nodeId);

// Expansion
store.expandNode(nodeId);
store.collapseNode(nodeId);
store.toggleNode(nodeId);
store.expandAll();
store.collapseAll();
store.expandPath("/src/components"); // Expands all parents

// Selection
store.selectNode(nodeId);
store.deselectNode(nodeId);
store.toggleSelection(nodeId);
store.selectAll();
store.clearSelection();
store.selectRange(fromId, toId); // Select visible nodes in range

Queries

// Get nodes
const node = store.getNode(nodeId);
const node = store.getNodeByPath("/src/index.ts");
const children = store.getChildren(nodeId);
const parent = store.getParent(nodeId);
const siblings = store.getSiblings(nodeId);

// Get collections
const visible = store.getVisibleNodes();
const selected = store.getSelectedNodes();
const cursor = store.getCursorNode();

// Navigation helpers
const next = store.getNextVisible(nodeId);
const prev = store.getPreviousVisible(nodeId);

// State checks
const expanded = store.isExpanded(nodeId);
const selected = store.isSelected(nodeId);
const visible = store.isVisible(nodeId);
const hasKids = store.hasChildren(nodeId);

File System Integration

The state automatically syncs with the file system when provided:

const store = createZustandTreeStore(fs);

// Watches all changes
fs.writeFile("/new-file.txt", "content");
// State auto-updates

fs.mkdir("/new-folder");
// State auto-updates

fs.deleteFile("/old-file.txt");
// State auto-updates

// Manual sync for specific events
store.syncWithFs({
  type: "created",
  path: "/manual-file.txt"
});

// Load from entries
const entries = await fs.readDir("/");
store.loadFromFs(entries);

Advanced Usage

Custom Selectors (Zustand)

import { treeSelectors } from "@node-tree/state";

// Use predefined selectors
const visibleNodes = useTreeStore(treeSelectors.visibleNodes);
const cursorNode = useTreeStore(treeSelectors.cursorNode);
const children = useTreeStore(treeSelectors.childrenOf("src"));

// Create custom selectors
const fileCount = useTreeStore(state => 
  Array.from(state.nodes.values()).filter(n => n.type === "file").length
);

Keyboard Navigation Example

function useKeyboardNavigation(store) {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      switch (e.key) {
        case "ArrowUp":
          e.preventDefault();
          store.moveCursorUp();
          break;
        case "ArrowDown":
          e.preventDefault();
          store.moveCursorDown();
          break;
        case "ArrowRight":
          const cursor = store.getCursorNode();
          if (cursor?.type === "directory") {
            store.expandNode(cursor.id);
          }
          break;
        case "ArrowLeft":
          const current = store.getCursorNode();
          if (current?.type === "directory" && store.isExpanded(current.id)) {
            store.collapseNode(current.id);
          } else if (current?.parentId) {
            store.setCursor(current.parentId);
          }
          break;
        case " ":
          e.preventDefault();
          const node = store.getCursorNode();
          if (node) store.toggleSelection(node.id);
          break;
      }
    };
    
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [store]);
}

Performance Tips

  1. Use selectors for computed values instead of filtering in components
  2. Batch operations when making multiple state changes
  3. Virtualize long lists of visible nodes (use @tanstack/react-virtual or similar)
  4. Debounce rapid file system changes if needed
  5. Memoize expensive computations in React components
  6. Use shallow equality in Zustand selectors for better performance

API Reference

TreeStore Interface

interface TreeStore {
  // State
  nodes: Map<string, TreeNode>;
  rootIds: string[];
  cursor?: string;
  selectedIds: Set<string>;
  expandedIds: Set<string>;
  visibleIds: string[];
  
  // All methods listed above...
}

Store Creation Options

// Zustand
const store = createZustandTreeStore(fs?: IFileSystem);

// MobX  
const store = createMobXTreeStore(fs?: IFileSystem);

// Vanilla (no reactivity)
const store = new TreeStateCore();

React Integration Examples

Creating a File Explorer Component

// FileExplorer.tsx
import { useTreeStore } from './store';
import { useKeyboardNavigation } from './hooks';

export function FileExplorer() {
  const visibleNodes = useTreeStore(state => state.getVisibleNodes());
  const cursor = useTreeStore(state => state.cursor);
  const { expandNode, collapseNode, setCursor } = useTreeStore();
  
  useKeyboardNavigation();
  
  return (
    <div className="file-explorer">
      {visibleNodes.map(node => (
        <FileNode
          key={node.id}
          node={node}
          isCursor={cursor === node.id}
          onToggle={() => node.type === 'directory' && toggleNode(node.id)}
          onClick={() => setCursor(node.id)}
        />
      ))}
    </div>
  );
}

Using with Zustand DevTools

import { devtools } from 'zustand/middleware';

const useTreeStore = create<TreeStore>()(
  devtools(
    subscribeWithSelector((set, get) => ({
      // ... your store
    })),
    { name: 'tree-store' }
  )
);

Common Patterns

Lazy Loading Directories

// Only load directory contents when expanded
const handleExpand = async (nodeId: string) => {
  const node = store.getNode(nodeId);
  if (node?.type === 'directory' && !node.children?.length) {
    const children = await fs.readDir(node.path);
    // Update store with new children
  }
  store.expandNode(nodeId);
};

Search Implementation

// Add search functionality
const searchNodes = (query: string) => {
  const results: TreeNode[] = [];
  for (const node of store.nodes.values()) {
    if (node.name.toLowerCase().includes(query.toLowerCase())) {
      results.push(node);
      // Expand parent path to show result
      store.expandPath(node.path);
    }
  }
  return results;
};

Troubleshooting

Issue: State not updating in React component

Solution: Make sure you're using the store correctly:

// ❌ Wrong - not reactive
const nodes = store.getVisibleNodes();

// ✅ Correct - reactive subscription
const nodes = useTreeStore(state => state.getVisibleNodes());

Issue: Too many re-renders

Solution: Use selectors to minimize updates:

// ❌ Causes re-render on any state change
const state = useTreeStore();

// ✅ Only re-renders when visible nodes change
const visibleNodes = useTreeStore(state => state.getVisibleNodes());

Issue: File system changes not reflecting

Solution: Ensure filesystem has watch support:

// Check if watch is working
const watcher = fs.watch('**', (event) => {
  console.log('File changed:', event);
});

Issue: Performance with large directories

Solution: Implement virtualization:

import { useVirtualizer } from '@tanstack/react-virtual';

// Only render visible items
const virtualizer = useVirtualizer({
  count: visibleNodes.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 24,
});

Contributing

See ARCHITECTURE.md for detailed technical documentation.

License

MIT