How to Handle IPC Communication Between Rust Backend and React Frontend in Tauri

Tauri enables seamless IPC communication between Rust and React by exposing backend functions through #[command] attributes, registering them in tauri::generate_handler!, and invoking them from the frontend using invoke() from @tauri-apps/api/core.

The lencx/ChatGPT desktop application demonstrates production-ready patterns for bidirectional IPC communication between a Rust backend and React frontend using Tauri's native bridge. By leveraging command handlers and webview APIs, the codebase establishes a type-safe, asynchronous channel that handles window management, configuration persistence, and cross-context script execution.

Defining Commands in the Rust Backend

Rust functions become callable from the frontend when annotated with #[command] and placed in src-tauri/src/core/cmd.rs. Each command receives an AppHandle (providing access to the Tauri application state) and any JSON-serializable arguments.

The following example reloads the main webview by executing JavaScript inside it:

// src-tauri/src/core/cmd.rs
#[command]
pub fn view_reload(app: AppHandle) {
    app.get_window("core")
        .unwrap()
        .get_webview("main")
        .unwrap()
        .eval("window.location.reload()")
        .unwrap();
}

Commands can also return data to the frontend. All return types must implement serde::Serialize:

#[command]
pub fn view_url(app: AppHandle) -> tauri::Url {
    app.get_window("core")
        .unwrap()
        .get_webview("main")
        .unwrap()
        .url()
        .unwrap()
}

Registering Commands with the Tauri Runtime

Exposed functions must be explicitly registered in src-tauri/src/main.rs using tauri::generate_handler!. Only commands listed in this macro become accessible from JavaScript.

// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_os::init())
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![
            cmd::view_reload,
            cmd::view_url,
            cmd::get_app_conf,
            cmd::window_pin,
            cmd::ask_sync,
            cmd::ask_send,
            cmd::set_theme,
            window::open_settings,
        ])
        .setup(setup::init)
        .run(tauri::generate_context!())
        .expect("error while running lencx/ChatGPT application");
}

Invoking Commands from the React Frontend

The React frontend imports invoke from @tauri-apps/api/core to call registered Rust functions. Commands return Promises that resolve with deserialized data.

In src/view/Titlebar.tsx, navigation and window controls trigger Rust handlers:

import { invoke } from '@tauri-apps/api/core';

const handleRefresh = () => {
  // Fire command without arguments
  invoke('view_reload');
};

const handlePin = (isPin: boolean) => {
  // Pass serializable payload
  invoke('window_pin', { pin: isPin });
};

const loadConf = async () => {
  // Await return value with typed interface
  const conf = await invoke<I.AppConf>('get_app_conf');
  setEnableAsk(conf.ask_mode);
  setPin(conf.stay_on_top);
};

Executing JavaScript from Rust via Webview::eval

When the backend needs to manipulate the DOM or trigger frontend functions directly, it uses Webview::eval to run arbitrary JavaScript inside a specific webview context.

The ask_send command in cmd.rs demonstrates this pattern by calling a frontend method and managing focus:

#[command]
pub fn ask_send(app: AppHandle) {
    let win = app.get_window("core").unwrap();

    win.get_webview("main")
        .unwrap()
        .eval(
            r#"
            ChatAsk.submit();
            setTimeout(() => {
                __TAURI__.webview.Webview.getByLabel('ask')?.setFocus();
            }, 500);
            "#,
        )
        .unwrap();
}

The React side simply invokes the command, while Rust handles the complex coordination between webviews:

// src/view/Ask.tsx
const sendMessage = async (msg: string) => {
  await invoke('ask_sync', { message: JSON.stringify(msg) });
  await invoke('ask_send');
};

Managing Multi-Webview Layouts via IPC

The application splits its UI into three webviews—main, titlebar, and ask—and uses IPC to orchestrate their geometry. The set_view_ask command adjusts positions and sizes based on the "ask mode" toggle state, using platform-specific logic for macOS.

#[command]
pub fn set_view_ask(app: AppHandle, enabled: bool) {
    // Calculate heights and positions
    // Adjust each webview with set_position and set_size
    // Handle macOS differences with #[cfg(target_os = "macos")]
}

From the frontend, toggling the Ask pane triggers the layout update:

const handleAsk = () => {
  const newState = !enableAsk;
  setEnableAsk(newState);
  invoke('set_view_ask', { enabled: newState });
};

Handling Configuration Round-Trips

Configuration flows bidirectionally through the IPC bridge. On startup, the frontend requests the application configuration:

invoke<I.AppConf>('get_app_conf').then(conf => {
  setEnableAsk(conf.ask_mode);
  setPin(conf.stay_on_top);
});

When users modify settings, the frontend invokes update commands that persist changes to a TOML file via Rust. Some commands, like set_theme, may trigger an app.restart() after saving to apply changes at the native level.

Summary

  • Define exposed functions using #[command] in src-tauri/src/core/cmd.rs, accepting AppHandle and serializable parameters.
  • Register all commands in main.rs via tauri::generate_handler! to make them callable from JavaScript.
  • Invoke commands from React using invoke('command_name', payload) from @tauri-apps/api/core, handling returned Promises for async data.
  • Execute frontend JavaScript from Rust using Webview::eval for direct DOM manipulation or function calls.
  • Orchestrate complex multi-webview layouts by calculating geometries in Rust and applying them through the webview API.

Frequently Asked Questions

How do you expose a Rust function to the React frontend in Tauri?

Annotate the function with #[command] in your Rust source (e.g., src-tauri/src/core/cmd.rs), ensure it accepts an AppHandle and serializable arguments, then register it in main.rs using tauri::generate_handler!. Only functions explicitly listed in the handler macro become accessible to the frontend.

How does the React frontend receive data from Rust commands?

The invoke function returns a Promise that resolves with the deserialized return value. Define the Rust command to return a type implementing serde::Serialize, then await the result in React: const data = await invoke<YourType>('command_name').

How can Rust execute JavaScript code inside the React frontend?

Use the eval method on a Webview instance obtained via app.get_window("label").unwrap().get_webview("label").unwrap().eval("js code"). This injects and executes the script in the specified webview's context, allowing Rust to trigger frontend functions or manipulate the DOM directly.

How does the lencx/ChatGPT app handle multiple webviews via IPC?

The app assigns unique labels (main, titlebar, ask) to separate webviews. Rust commands like set_view_ask retrieve each webview by label, then use set_position and set_size to recalculate layouts. The frontend triggers these layout updates by invoking commands with boolean flags or configuration objects.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →