Returning Data to React

Learn how to return primitive values, structs, vectors, and JSON from Tauri Rust commands to your React frontend, with serialization and error handling.

When a Tauri command finishes, Rust sends the return value back to the frontend as a serialized JSON payload. The JavaScript side receives the data inside a resolved Promise. This page covers every common return type — strings, custom structs, arrays, dynamic JSON — and how to shape that data so your React components can consume it cleanly.

Serialization is automatic:

Tauri uses serde under the hood. Any type that implements serde::Serialize can be returned from a command. The frontend receives a plain JavaScript value — a string, number, object, or array — without any extra conversion code.

Returning Primitive Values

The simplest commands return a single string, integer, float, or boolean. Tauri maps these directly to the equivalent JavaScript primitive.

Rust — src-tauri/src/lib.rs

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
#[tauri::command]
fn double_input(n: i32) -> i32 {
    n * 2
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, double_input])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

React — src/App.jsx

import { invoke } from '@tauri-apps/api/core';
import { useState, useEffect } from 'react';
function App() {
  const [greeting, setGreeting] = useState('');
  const [doubled, setDoubled] = useState(null);
  useEffect(() => {
    invoke('greet', { name: 'Tauri' }).then((message) => setGreeting(message));
    invoke('double_input', { n: 21 }).then((result) => setDoubled(result));
  }, []);
  return (
    <div>
      <p>{greeting}</p>
      {doubled !== null && <p>21 doubled is {doubled}</p>}
    </div>
  );
}
export default App;

The invoke call returns a Promise that resolves with the Rust return value. The argument object keys must match the command parameter names exactly — Tauri uses those names, not positions, to bind values.

Everything is working:

If you see Hello, Tauri! and 42 in the browser, you've successfully returned primitive data from Rust to React.

Returning Custom Structs

Real applications return structured data. Define a struct in Rust, derive Serialize, and return it from a command. On the frontend you get a plain object with the same keys.

Rust — src-tauri/src/lib.rs

use serde::Serialize;
#[derive(Serialize)]
struct User {
    id: u32,
    name: String,
}
#[tauri::command]
fn get_user() -> User {
    User {
        id: 1,
        name: "Alice".to_string(),
    }
}

Don't forget to register the command in the builder:

.invoke_handler(tauri::generate_handler![get_user])

React — src/App.jsx

import { invoke } from '@tauri-apps/api/core';
import { useState, useEffect } from 'react';
interface User {
  id: number;
  name: string;
}
function App() {
  const [user, setUser] = useState<User | null>(null);
  useEffect(() => {
    invoke<User>('get_user').then(setUser);
  }, []);
  return (
    <div>
      {user && (
        <p>User: {user.name} (ID: {user.id})</p>
      )}
    </div>
  );
}

TypeScript generics on invoke<User> give you type safety — the returned object is assumed to match the User interface you define. Tauri does not enforce this at runtime; the shape depends entirely on your Rust struct.

Forgotten Serialize derive:

If you forget #[derive(Serialize)], the compiler will refuse to build. The error message will mention that the type doesn't implement Serialize. Always check that every type you return — and every field it contains — derives Serialize.

Returning Vectors and Collections

When a command returns a Vec<T>, the frontend receives a JavaScript array. Each element is serialized as an object if T is a struct, or as a primitive if T is a simple type.

Rust — src-tauri/src/lib.rs

#[tauri::command]
fn list_users() -> Vec<User> {
    vec![
        User { id: 1, name: "Alice".into() },
        User { id: 2, name: "Bob".into() },
    ]
}

React — src/App.jsx

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  useEffect(() => {
    invoke<User[]>('list_users').then(setUsers);
  }, []);
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Nothing special is required — Tauri serializes the vector as a JSON array.

Large datasets:

Returning thousands of entries serializes everything into a single JSON string and sends it across the IPC bridge. For large collections consider pagination, windowing, or returning only the subset you need. The IPC call is blocking on the Rust side while serialization runs, and very large payloads can make the UI feel sluggish.

Returning Raw JSON Data

Sometimes you need to return data whose shape you don't know at compile time — configuration objects, API responses, or dynamic metadata. Use serde_json::Value to construct and return arbitrary JSON.

Rust — src-tauri/src/lib.rs

use serde_json::Value;
#[tauri::command]
fn get_settings() -> Value {
    serde_json::json!({
        "theme": "dark",
        "notifications": {
            "email": true,
            "push": false
        },
        "version": 2
    })
}

React — src/App.jsx

type Settings = {
  theme: string;
  notifications: { email: boolean; push: boolean };
  version: number;
};
function App() {
  const [settings, setSettings] = useState<Settings | null>(null);
  useEffect(() => {
    invoke<Settings>('get_settings').then(setSettings);
  }, []);
  // ...
}

The frontend sees the same nested object structure. Because serde_json::Value already implements Serialize, you can return it directly — no extra derive needed.

Formatting Responses with Result

Commands that can fail should return a Result<T, E>. On success, Tauri resolves the JavaScript Promise with the inner value. On failure, the Promise rejects, and the error is thrown on the frontend — you can catch it with .catch() or try/catch.

Rust — src-tauri/src/lib.rs

#[tauri::command]
fn get_user_by_id(id: u32) -> Result<User, String> {
    if id == 0 {
        Err("User not found".into())
    } else {
        Ok(User {
            id,
            name: "Alice".into(),
        })
    }
}

React — src/App.jsx

function UserFetcher() {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);
  const fetchUser = (id: number) => {
    invoke<User>('get_user_by_id', { id })
      .then(setUser)
      .catch((err) => setError(String(err)));
  };
  // ...
}

Unhandled rejections:

If you call invoke and don't attach a .catch() handler, the frontend will see an unhandled promise rejection. In development this appears as a console warning; in production it could go unnoticed and leave your UI in an inconsistent state. Always handle the error case.

A String error type is fine for prototyping. For production, define a custom error type that implements both std::error::Error and serde::Serialize. Tauri will serialize the error object and make it available in the rejection reason:

#[derive(Debug, thiserror::Error, Serialize)]
enum AppError {
    #[error("User not found with id {0}")]
    NotFound(u32),
    #[error("Permission denied")]
    PermissionDenied,
}
#[tauri::command]
fn get_user_or_error(id: u32) -> Result<User, AppError> {
    if id == 0 {
        Err(AppError::NotFound(id))
    } else {
        Ok(User { id, name: "Alice".into() })
    }
}

The frontend receives a rejected Promise whose value is the serialized error object:

// { "NotFound": [0] }  for a NotFound variant

You can pattern‑match on the error structure to show meaningful messages.

Panics crash the app:

A Rust panic inside a command kills the entire Tauri process — the window disappears. Instead of panicking, return Err with a descriptive message. Use Result even when you think a failure is impossible; it costs nothing and keeps the frontend safe.


Summary

You now have a clear map for every kind of data you need to send from Rust to React:

  • Primitives — return strings, numbers, booleans. They appear as plain JS values.
  • Structs — derive Serialize; the frontend gets an object with matching keys.
  • Vectors — return a Vec<T>; the frontend gets an array.
  • Dynamic JSON — use serde_json::Value when the shape is not fixed.
  • Result typesOk resolves the Promise, Err rejects it. Custom error types give you structured error objects.