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