p.enthalabs

use-fs • your folder is now state

Watch a folder.

Re-render on change.

Point useFs() at a folder on your machine, or at the browser's own private storage. Your component gets everything inside it, and re-renders the moment a file is added, changed or deleted. No upload, no refresh, no second file dialog.

Full playground

Folder on disk

Chrome · Edge · Opera, desktop

Browser storage

Chrome · Edge · Opera · Safari · Firefox

Talks to

Nothing. No server, no upload

Licence

MIT

(newFiles, previousFiles)

A path the previous scan did not have. Filters run first, so a pruned directory never even gets enumerated.

(changedFiles, previousFiles)

Same path, different bytes. Contents are only re-read when lastModified or size moved, so a steady tree costs no I/O.

(deletedFiles, previousFiles)

The path is gone. You still get its last contents, so you can archive it, undo it, or put it back.

Two stores, one hook

A folder on disk, or the browser's own.

Both are a FileSystemDirectoryHandle underneath, so the same files map, the same filters and the same writeFile work against either. Watch one, the other, or both at once.

The user picks a directory and grants access to it. Your app reads and writes the real files — the ones already open in their editor.

Browsers Desktop Chrome, Edge, Opera

Asks A picker, then a permission prompt to write

Stored On your disk, where your editor can see them

This browser has no directory picker. The store on the right works here.

The origin private file system: a real directory tree the user never sees, scoped to your origin and kept across reloads. Nothing to approve, so it can mount in an effect.

Browsers Chrome, Edge, Opera, Safari 17+, Firefox 111+

Asks Nothing at all

Stored In the browser, private to this origin

Pass name. The OPFS root is shared with everything else on the origin — WASM databases, other libraries — and mounting it whole means walking all of it on every scan.

The whole integration

One hook, one folder.

What one scan does

Every 300ms, in order.

1. 01

Walk

Breadth-first, with a bounded number of directories open at once. A filter that rejects a directory prunes the whole subtree, so node_modules is never enumerated.

2. 02

Stat

Every discovered file is stat'd first. Contents are re-read only when lastModified or size moved, so polling a large tree at rest does no content I/O.

3. 03

Diff

Added, changed and deleted are resolved against the previous scan. Rendered state is coalesced by debounceInterval; callbacks always fire immediately.

Scans never throw. A directory that cannot be enumerated — permission revoked, folder moved — keeps its last known contents instead of reporting every file as deleted, and the reason is surfaced through error.

Full view

Read it, edit it, write it back.

The same directory the panel at the top is watching — a folder off your disk, or the browser's own storage. Everything runs in this tab; nothing is uploaded, and disk access ends when you close it.

This browser has neither the File System Access API nor the origin private file system. Open the page in a current Chrome, Edge, Opera, Safari or Firefox to use the playground.

0 files paused

Files

Nothing open yet.

Pick a folder, or mount browser storage — nothing is read until you do. commonFilters skips node_modules, build output and anything your .gitignore lists.

Viewer

No file selected.

Pick one on the left to read it. Change it in your editor and the diff appears here on the next scan.

Events

Nothing yet.

Added and deleted files land here, newest first, as each scan resolves.

Every export

The entire API.

State

files Map<string, string> of watched files, keyed by path

handles Map of FileSystemFileHandle, keyed by path

directories Paths of the watched roots

isProcessing A scan has run long enough to be worth showing

isPolling The polling loop is running

isBrowserSupported The directory picker is available

isOpfsSupported The origin private file system is available

error Most recent recoverable error, or null

Actions

onDirectorySelection()Open the picker and watch the choice

addOpfsDirectory(options?)Watch browser storage — no prompt, no gesture

addDirectory(handle, options?)Watch a handle you already hold

removeDirectory(path)Stop watching, without touching disk

refresh()Run a scan right now

startPolling() / stopPolling()Drive the loop by hand

writeFile(path, data, options?)Write, creating missing parents

createFile(path, initialData?)Create or open, returns the handle

deleteFile(path)Delete one file

deleteDirectory(path)Delete a directory and everything below

requestPermission(mode?)Re-request access for every root

onClear()Stop watching everything and reset

Options · default

filters commonFilters

pollInterval 300

debounceInterval 50

batchSize 50

concurrency 8

mode"read"

autoStartPolling true

processingIndicatorDelay 100

Filters

Decide what the hook can see.

commonFilters is the default: it prunes build output, drops.DS_Store and friends, and honours every .gitignore in the tree. Compose your own on top — a filter that rejects a directory prunes the whole subtree.

Also exported: walkDirectory, scanDirectories, toContentMap, normalizePath, isFileSystemAccessSupported, isOpfsSupported, getDirectoryPicker, getOpfsRoot, ensurePermission.