Skip to content
Documentation GitHub
Development Guides

Build Frontend Components

Guide for adding new React components to the desktop frontend.

Prerequisites

  • Understand the frontend component tree, state management, and provider hierarchy
  • Understand the dumb pipe principle: all business logic lives in Rust — React handles only UI/UX

Key Principles

  1. No domain logic in TypeScript — validation, data transformation, and business rules belong in Rust use cases
  2. All data via invoke() — every read/write goes through Tauri IPC to the backend
  3. Zustand for cross-component state — use appStore for UI state (navigation, dialogs, toasts, layout)
  4. React Context for lifecycle stateWorkspaceProvider and SyncProvider wrap the app

Step 1: Choose the Right Location

src-react/components/
├── layout/ # App shell structure (sidebar, panels)
├── editor/ # Editor and extensions
├── context/ # Right-panel context components
├── tags/ # Tag management
├── settings/ # Settings panels
├── attachments/ # Attachment browser
├── auth/ # Auth UI
├── sync/ # Sync status
├── CommandPalette/ # Command palette
├── FirstLaunch/ # Onboarding
└── [Dialog components]

Place your component in the appropriate feature directory. If it doesn’t fit, create a new directory.

Step 2: Data Fetching Pattern

Use invoke() with Specta-generated type-safe bindings:

import { commands } from "@/lib/bindings";
// Type-safe — return type and parameters are generated from Rust
const page = await commands.getPage(slug);

For components that need reactive data, use a refresh key pattern:

const pageTreeKey = useAppStore((s) => s.pageTreeKey);
useEffect(() => {
// Re-fetch when pageTreeKey changes (after mutations elsewhere)
loadPages();
}, [pageTreeKey]);

Step 3: Error Handling

import { parseCommandError } from "@/lib/errors";
try {
await commands.createPage({ title, parentSlug });
} catch (err) {
addToast(parseCommandError(err)); // Always user-friendly
}

See Error Handling Patterns for the full error hierarchy and frontend handling.

Step 4: State Management

For local component state, use React useState. For cross-component state, use the Zustand stores:

import { useAppStore } from "@/stores/appStore";
// Read state
const selectedSlug = useAppStore((s) => s.selectedPageSlug);
// Dispatch actions
const { selectPage, addToast } = useAppStore();

See apps/desktop/src-react/stores/appStore.ts for the full store reference.

Step 5: Connect to Backend Commands

If your component needs a new backend command, follow Adding a Tauri Command to create the full vertical slice.

Checklist

  • Component placed in correct feature directory
  • All data fetched via invoke() / Specta bindings (no direct DB/filesystem access)
  • Errors handled with parseCommandError(), shown via toast
  • Cross-component state uses appStore (not prop drilling)
  • No business logic in TypeScript (validation, rules in Rust)
  • pnpm typecheck passes

Was this page helpful?