Fiber UI LogoFiberUI

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

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

File System Access API is not supported in this browser.
"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

PropertyTypeDescription
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
fileFile | nullThe currently loaded File object
fileHandleFileSystemFileHandle | nullThe persistent handle for the open file
isSupportedbooleantrue if File System Access API is supported
isLoadingbooleantrue during file operations
errorError | nullError 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;