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

> Learn to handle IPC communication between your Rust backend and React frontend in Tauri. Tauri simplifies this with #[command] attributes and invoke() for seamless integration.

- Repository: [lencx/ChatGPT](https://github.com/lencx/ChatGPT)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/lencx/ChatGPT/blob/main/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:

```rust
// 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`:

```rust
#[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`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/main.rs) using `tauri::generate_handler!`. Only commands listed in this macro become accessible from JavaScript.

```rust
// 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`](https://github.com/lencx/ChatGPT/blob/main/src/view/Titlebar.tsx), navigation and window controls trigger Rust handlers:

```tsx
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`](https://github.com/lencx/ChatGPT/blob/main/cmd.rs) demonstrates this pattern by calling a frontend method and managing focus:

```rust
#[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:

```tsx
// 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.

```rust
#[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:

```tsx
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:

```tsx
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`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/cmd.rs), accepting `AppHandle` and serializable parameters.
- **Register** all commands in [`main.rs`](https://github.com/lencx/ChatGPT/blob/main/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`](https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/core/cmd.rs)), ensure it accepts an `AppHandle` and serializable arguments, then register it in [`main.rs`](https://github.com/lencx/ChatGPT/blob/main/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.