use-fs • your folder is now state

4 min read Original article ↗

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

onFilesAdded

(newFiles, previousFiles)

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

onFilesChanged

(changedFiles, previousFiles)

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

onFilesDeleted

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

A folder on disk

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.

Browser storage

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

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.