useFileSystem
A powerful storage hook for reading, writing, and managing files directly on the user's local file system using the modern File System Access API.
Installation
npx shadcn@latest add https://r.fiberui.com/r/hooks/use-file-system.jsonA React hook that provides access to the File System Access API, enabling web apps to read, write, and save changes directly to files and directories on the user's device.
Browser Support
This API is currently supported primarily in Desktop Chrome, Edge, and
Opera. Firefox and Safari have limited or no support. Always check
isSupported before using.
Features
- Read/Write - Open files, read content, and write changes back
- Save As - Create new files with the "Save As" picker
- File Handles - Maintains persistent handles to files for subsequent writes
- SSR Safe - Works safely with server-side rendering
Basic Usage
A simple text editor that can open local files, edit them, and save changes back to disk.
"use client";
import { useFileSystem } from "@repo/hooks/storage/use-file-system";
import { Button } from "@repo/ui/components/button";
import { FileText, Save, FolderOpen } from "lucide-react";
import { useState, useEffect } from "react";
import { toast } from "sonner";
export function Example1() {
const { openFile, saveFile, saveFileAs, file, isLoading, isSupported } =
useFileSystem();
const [content, setContent] = useState("");
// Read file content when file changes
useEffect(() => {
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setContent((e.target?.result as string) || "");
};
reader.readAsText(file);
}
}, [file]);
const handleOpen = async () => {
try {
await openFile({
types: [
{
description: "Text Files",
accept: {
"text/plain": [".txt", ".md", ".json"],
},
},
],
});
} catch (err) {
console.error(err);
}
};
const handleSave = async () => {
if (!file) {
handleSaveAs();
return;
}
const success = await saveFile(content);
if (success) toast.success("File saved successfully!");
};
const handleSaveAs = async () => {
const success = await saveFileAs(content, {
types: [
{
description: "Text Files",
accept: {
"text/plain": [".txt"],
},
},
],
});
if (success) toast.success("File saved as new file!");
};
if (!isSupported) {
return (
<div className="rounded-md bg-yellow-100 p-4 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-200">
File System Access API is not supported in this browser.
</div>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2 border-b pb-4">
<Button
onClick={handleOpen}
isDisabled={isLoading}
variant="outline"
>
<FolderOpen className="mr-2 h-4 w-4" />
Open File
</Button>
<Button onClick={handleSave} isDisabled={isLoading}>
<Save className="mr-2 h-4 w-4" />
Save
</Button>
<Button
onClick={handleSaveAs}
isDisabled={isLoading}
variant="secondary"
>
<FileText className="mr-2 h-4 w-4" />
Save As...
</Button>
{file && (
<div className="text-muted-foreground ml-auto text-sm">
Editing:{" "}
<span className="text-foreground font-medium">
{file.name}
</span>
</div>
)}
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="focus:ring-ring min-h-[200px] w-full rounded-md border bg-transparent p-4 font-mono text-sm shadow-sm outline-none focus:ring-1"
placeholder="Type something or open a text file..."
/>
</div>
);
}
API Reference
Hook Signature
function useFileSystem(): UseFileSystemReturn;Return Value
| Property | Type | Description |
|---|---|---|
openFile | (options?) => Promise<File | null> | Opens file picker and loads selected file |
saveFile | (content) => Promise<boolean> | Saves content to the currently open file handle |
saveFileAs | (content, options?) => Promise<boolean> | Opens "Save As" picker and saves content to new file |
file | File | null | The currently loaded File object |
fileHandle | FileSystemFileHandle | null | The persistent handle for the open file |
isSupported | boolean | true if File System Access API is supported |
isLoading | boolean | true during file operations |
error | Error | null | Error object if an operation fails |
Options
interface OpenFileOptions {
types?: FilePickerAcceptType[]; // Allowed file types
multiple?: boolean; // Allow multiple selection (returns first currently)
description?: string; // Description for picker
}
interface SaveFileOptions {
suggestedName?: string; // Default file name
types?: FilePickerAcceptType[]; // Allowed file types
}Hook Source Code
import { useState, useCallback } from "react";
/**
* Options for file picking
*/
export interface OpenFileOptions {
/** Allowed MIME types or file extensions */
types?: FilePickerAcceptType[];
/** Allow multiple file selection */
multiple?: boolean;
/** excluded types */
excludeAcceptAllOption?: boolean;
/** Description for the file picker */
description?: string;
}
/**
* Options for file saving
*/
export interface SaveFileOptions {
/** Suggested file name */
suggestedName?: string;
/** Allowed MIME types */
types?: FilePickerAcceptType[];
}
/**
* Return type for the useFileSystem hook
*/
export interface UseFileSystemReturn {
/** Open a file picker and load the file */
openFile: (options?: OpenFileOptions) => Promise<File | null>;
/** Save content to the currently open file */
saveFile: (content: string | Blob | BufferSource) => Promise<boolean>;
/** Save content to a new file (Save As) */
saveFileAs: (
content: string | Blob | BufferSource,
options?: SaveFileOptions,
) => Promise<boolean>;
/** The currently active file handle */
fileHandle: FileSystemFileHandle | null;
/** The currently loaded file object */
file: File | null;
/** Whether the API is supported */
isSupported: boolean;
/** Whether an operation is in progress */
isLoading: boolean;
/** Error from last operation */
error: Error | null;
}
// Types for File System Access API
// These might be available in newer TypeScript versions or DOM libs,
// but defining them here ensures compatibility.
interface FilePickerAcceptType {
description?: string;
accept: Record<string, string[]>;
}
interface OpenFilePickerOptions {
multiple?: boolean;
excludeAcceptAllOption?: boolean;
types?: FilePickerAcceptType[];
}
interface SaveFilePickerOptions {
suggestedName?: string;
types?: FilePickerAcceptType[];
}
declare global {
interface Window {
showOpenFilePicker?: (
options?: OpenFilePickerOptions,
) => Promise<FileSystemFileHandle[]>;
showSaveFilePicker?: (
options?: SaveFilePickerOptions,
) => Promise<FileSystemFileHandle>;
}
}
/**
* A React hook that provides access to the File System Access API,
* allowing reading and writing files directly to the user's system.
*
* @returns UseFileSystemReturn and state
*/
export function useFileSystem(): UseFileSystemReturn {
const [fileHandle, setFileHandle] = useState<FileSystemFileHandle | null>(
null,
);
const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Check support
const isSupported =
typeof window !== "undefined" && "showOpenFilePicker" in window;
// Open file
const openFile = useCallback(
async (options: OpenFileOptions = {}): Promise<File | null> => {
if (!isSupported) {
setError(new Error("File System Access API is not supported"));
return null;
}
setIsLoading(true);
setError(null);
try {
// @ts-ignore - API support check handles this
const handles = await window.showOpenFilePicker({
multiple: false, // We only support single file for this simple hook for now
types: options.types,
excludeAcceptAllOption: options.excludeAcceptAllOption,
});
if (!handles || handles.length === 0 || !handles[0]) {
setIsLoading(false);
return null;
}
const handle = handles[0];
const fileData = await handle.getFile();
setFileHandle(handle);
setFile(fileData);
return fileData;
} catch (err) {
// Ignore abort errors (user cancelled)
if ((err as Error).name !== "AbortError") {
const error =
err instanceof Error
? err
: new Error("Failed to open file");
setError(error);
}
return null;
} finally {
setIsLoading(false);
}
},
[isSupported],
);
// Write content to a handle
const writeToHandle = async (
handle: FileSystemFileHandle,
content: string | Blob | BufferSource,
) => {
// Create a writable stream
// @ts-ignore
const writable = await handle.createWritable();
// Write the contents
await writable.write(content);
// Close the file
await writable.close();
};
// Save to current file
const saveFile = useCallback(
async (content: string | Blob | BufferSource): Promise<boolean> => {
if (!isSupported) {
setError(new Error("File System Access API is not supported"));
return false;
}
if (!fileHandle) {
setError(new Error("No file currently open"));
return false;
}
setIsLoading(true);
setError(null);
try {
await writeToHandle(fileHandle, content);
// Refresh file data
const updatedFile = await fileHandle.getFile();
setFile(updatedFile);
return true;
} catch (err) {
const error =
err instanceof Error
? err
: new Error("Failed to save file");
setError(error);
return false;
} finally {
setIsLoading(false);
}
},
[isSupported, fileHandle],
);
// Save as new file
const saveFileAs = useCallback(
async (
content: string | Blob | BufferSource,
options: SaveFileOptions = {},
): Promise<boolean> => {
if (!isSupported) {
setError(new Error("File System Access API is not supported"));
return false;
}
setIsLoading(true);
setError(null);
try {
// @ts-ignore
const handle = await window.showSaveFilePicker({
suggestedName: options.suggestedName,
types: options.types,
});
await writeToHandle(handle, content);
const fileData = await handle.getFile();
setFileHandle(handle);
setFile(fileData);
return true;
} catch (err) {
if ((err as Error).name !== "AbortError") {
const error =
err instanceof Error
? err
: new Error("Failed to save file");
setError(error);
}
return false;
} finally {
setIsLoading(false);
}
},
[isSupported],
);
return {
openFile,
saveFile,
saveFileAs,
fileHandle,
file,
isSupported,
isLoading,
error,
};
}
export default useFileSystem;
React Hooks
A collection of production-ready React hooks for storage, forms, real-time communication, device access, and more. Type-safe, SSR-compatible, and designed for modern applications.
useIndexedDB
A React hook for persisting data in IndexedDB. Provides a simple API for storing large structured data sets asynchronously in the browser.