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::Valuewhen the shape is not fixed. - Result types —
Okresolves the Promise,Errrejects it. Custom error types give you structured error objects.