Reactive state management for virtual file systems
npm install @node-tree/stateReactive 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.
- ๐ฏ 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
``bash`
npm install @node-tree/state @firesystem/coreor
pnpm add @node-tree/state @firesystem/coreor
yarn add @node-tree/state @firesystem/core
Choose your preferred state management library:
`bashFor Zustand (recommended)
npm install zustand
Quick Start
$3
`typescript
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 (
{nodes.map(node => (
key={node.id}
style={{ paddingLeft: node.level * 20 }}
className={cursor === node.id ? 'selected' : ''}
>
{node.type === 'directory' && (
)}
{node.name}
))}
$3
`typescript
import { createMobXTreeStore } from "@node-tree/state";
import { observer } from "mobx-react-lite";const treeStore = createMobXTreeStore(fs);
const FileExplorer = observer(() => {
const nodes = treeStore.getVisibleNodes();
return (
{nodes.map(node => (
{node.name}
))}
);
});
`$3
`typescript
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
$3
`typescript
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
}
`$3
`typescript
// 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
`$3
`typescript
// 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:
`typescript
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
$3
`typescript
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
);
`$3
`typescript
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]);
}
`$3
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 performanceAPI Reference
$3
`typescript
interface TreeStore {
// State
nodes: Map;
rootIds: string[];
cursor?: string;
selectedIds: Set;
expandedIds: Set;
visibleIds: string[];
// All methods listed above...
}
`$3
`typescript
// Zustand
const store = createZustandTreeStore(fs?: IFileSystem);// MobX
const store = createMobXTreeStore(fs?: IFileSystem);
// Vanilla (no reactivity)
const store = new TreeStateCore();
`React Integration Examples
$3
`typescript
// 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 (
{visibleNodes.map(node => (
key={node.id}
node={node}
isCursor={cursor === node.id}
onToggle={() => node.type === 'directory' && toggleNode(node.id)}
onClick={() => setCursor(node.id)}
/>
))}
);
}
`$3
`typescript
import { devtools } from 'zustand/middleware';const useTreeStore = create()(
devtools(
subscribeWithSelector((set, get) => ({
// ... your store
})),
{ name: 'tree-store' }
)
);
`Common Patterns
$3
`typescript
// 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);
};
`$3
`typescript
// 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
$3
Solution: Make sure you're using the store correctly:
`typescript
// โ Wrong - not reactive
const nodes = store.getVisibleNodes();// โ
Correct - reactive subscription
const nodes = useTreeStore(state => state.getVisibleNodes());
`$3
Solution: Use selectors to minimize updates:
`typescript
// โ Causes re-render on any state change
const state = useTreeStore();// โ
Only re-renders when visible nodes change
const visibleNodes = useTreeStore(state => state.getVisibleNodes());
`$3
Solution: Ensure filesystem has watch support:
`typescript
// Check if watch is working
const watcher = fs.watch('**', (event) => {
console.log('File changed:', event);
});
`$3
Solution: Implement virtualization:
`typescript
import { useVirtualizer } from '@tanstack/react-virtual';// Only render visible items
const virtualizer = useVirtualizer({
count: visibleNodes.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 24,
});
``See ARCHITECTURE.md for detailed technical documentation.
MIT