All posts

Tauri, React, and Rust — the stack

Every app is a bet on a stack. Glifo bets on Tauri for the shell, React for the UI, and Rust for everything that touches the filesystem, git, or the network. Here's why.

Why Tauri

Tauri gives you a native window with a system webview (WKWebView on macOS) and a Rust backend. Compared to Electron:

ElectronTauri
App size~150 MB~8 MB
Memory~200 MB idle~40 MB idle
BackendNode.jsRust
WebviewBundled ChromiumSystem

The system webview means native performance, macOS integration (NSVisualEffectView, Keychain, system theme), and a tiny bundle. The trade-off is less control over the rendering engine — but WKWebView on modern macOS is excellent.

Why React

The UI is a single-page React app with CodeMirror 6 for the editor. React gives us:

  • Component composition for complex UI (sidebar, editor, preview, settings, publish wizard)
  • Fine-grained re-rendering with hooks and memoization
  • A massive ecosystem for things like markdown rendering (markdown-it) and syntax highlighting (highlight.js)

The alternative was a native SwiftUI or AppKit UI. But the web platform is better for rich text editing — CodeMirror, ProseMirror, and friends are decades ahead of native text frameworks for this use case.

Why Rust

Rust handles everything outside the browser:

  • File I/O — reading/writing notes, watching for changes
  • Git operations — commits, diffs, history via git2
  • Publishing — markdown parsing, HTML generation, GitHub API calls
  • Config management — TOML parsing, file watching, state management
  • Platform integration — NSVisualEffectView, Keychain, window management via objc crate

Rust's ownership model means no GC pauses during file operations. serde makes JSON/TOML serialization trivial. And Tauri's command system provides type-safe IPC between the frontend and backend.

The IPC boundary

React calls Rust through Tauri commands — async functions decorated with #[tauri::command]:

#[tauri::command]
pub async fn publish_to_github(
    notes: Vec<NoteToPublish>,
    repo: String,
    site: SiteConfig,
) -> Result<String, String> {
    // Rust handles all the work
}

On the frontend, it's a simple invoke:

const url = await invoke<string>('publish_to_github', { notes, repo, site });

Serialization happens automatically via serde. Types are checked at compile time on the Rust side. The frontend gets type safety through TypeScript interfaces that mirror the Rust structs.

What we'd change

State management could be simpler. We use a mix of React context, local state, and Tauri events. A more unified approach (Zustand or Jotai) would reduce prop drilling.

CSS architecture grew organically. We split globals.css into focused files, but a proper design token system would help as the UI grows.

Testing is the weakest link. Rust code has some tests, but the React layer is mostly untested. Integration tests for the IPC boundary would catch the most bugs.


The stack isn't perfect, but it's honest: use each tool for what it's best at. The browser for UI, Rust for systems work, and Tauri to glue them together.