We have all built or used web-based text and configuration tools. The absolute worst part of the user experience is the continuous download cycle. Want to save your progress? Click download. Need to make a quick adjustment? Download config (3).json again.
Modern browsers solve this with the File System Access API, which lets web applications read and write directly to files on the user's local disk. However, managing file handles, tracking reading/saving states, and handling user cancellations in React can introduce massive amounts of boilerplate.
In the latest release of react-hook-lab, we are excited to introduce the useFileSystem hook to handle all of this for you.
What is useFileSystem?
useFileSystem is a declarative, type-safe React hook that wraps the browser's native File System Access API. It allows you to prompt users to open a file, read its content directly into your state, and continuously write changes back to that same file on disk without prompting for re-downloads.
Here are some of its primary features:
-
Real-time Status Tracking: Easily handle UI states using the built-in states:
idle,picking,reading,saving, anderror. -
Standardized State Management: Keeps track of the active
Filemetadata, rawcontent, and the underlyingFileSystemFileHandle. - Smart Error Handling: Gracefully handles user cancellations (like closing the file picker) without throwing unhandled exceptions.
Let's dive into some practical code examples to see it in action.
Example 1: Creating a Basic File Reader & Writer
This simple component allows a user to open any text file, view its raw contents, and append a timestamped log line directly back to that same file on disk.
import React from "react";
import { useFileSystem } from "react-hook-lab";
export function SimpleLogWriter() {
const {
open,
save,
content,
status,
file,
isSupported
} = useFileSystem({
accept: { "text/plain": [".txt"] },
description: "Log Files"
});
if (!isSupported) {
return <p>Your browser does not support direct file system access.</p>;
}
const handleAppendLog = () => {
const existingContent = content || "";
const newLog = `${existingContent}\n[LOG] Updated at ${new Date().toLocaleTimeString()}`;
save(newLog);
};
return (
<div style={{ padding: "1rem", border: "1px solid #ccc", borderRadius: "8px" }}>
<h3>Direct-to-Disk Log Writer</h3>
<p>Status: <strong>{status}</strong></p>
{file && <p>Editing file: <code>{file.name}</code> ({file.size} bytes)</p>}
<div style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}>
<button onClick={() => open()}>Open Log File</button>
<button onClick={handleAppendLog} disabled={!file || status === "saving"}>
Append Log Entry
</button>
</div>
<pre style={{ background: "#f5f5f5", padding: "1rem", borderRadius: "4px" }}>
{content || "No file loaded. Click 'Open Log File' to load a text document."}
</pre>
</div>
);
}
Example 2: Markdown Editor with Save As Support
In a real-world scenario, you want to allow users to edit text in a text area, write changes to the original file, and trigger a "Save As" prompt when they want to create a new copy or rename it.
import React, { useState, useEffect } from "react";
import { useFileSystem } from "react-hook-lab";
export function MarkdownEditor() {
const {
open,
save,
saveAs,
content,
file,
reset,
status
} = useFileSystem({
accept: { "text/markdown": [".md"] },
description: "Markdown Documents"
});
const [editorText, setEditorText] = useState("");
// Keep local editor state in sync when a file is loaded
useEffect(() => {
if (content !== null) {
setEditorText(content);
}
}, [content]);
const handleNewFile = () => {
reset();
setEditorText("");
};
return (
<div>
<h2>{file ? `Editing: ${file.name}` : "New Document *"}</h2>
<textarea
value={editorText}
onChange={(e) => setEditorText(e.target.value)}
rows={10}
style={{ width: "100%", fontFamily: "monospace", padding: "0.5rem" }}
placeholder="Type your markdown here..."
/>
<div style={{ marginTop: "1rem", display: "flex", gap: "0.5rem" }}>
<button onClick={() => open()}>Open File</button>
<button onClick={() => save(editorText)} disabled={status === "saving"}>
Save
</button>
<button
onClick={() => saveAs(editorText, { suggestedName: "draft.md" })}
disabled={status === "saving"}
>
Save As...
</button>
<button onClick={handleNewFile}>New File (Reset)</button>
</div>
<p style={{ fontSize: "0.85rem", color: "#666" }}>Current status: {status}</p>
</div>
);
}
Wrapping Up
Web apps are becoming more powerful, blurring the lines between standard websites and full-fledged desktop applications. Direct-to-disk interaction using the useFileSystem hook is an incredible tool for developer portals, configuration managers, web-based editors, and local-first utilities.
Give it a try in your next project!
Resources
- NPM Package: https://www.npmjs.com/package/react-hook-lab
- GitHub Repository: https://github.com/Saurav-TB-Pandey/react-hook-lab
- LinkedIn Profile: https://www.linkedin.com/in/pandeysaurav/
Originally published on my blog. You can read the alternative breakdown here.
Top comments (0)