How iloader Provides Real-Time Progress Updates for Long-Running Operations

iloader leverages Tauri's cross-process event system to stream structured progress data from Rust to React, enabling instantaneous UI updates without polling.

iloader is a desktop application built with a React frontend and a Rust backend powered by Tauri. The application implements real-time progress updates for device operations by establishing an event-driven bridge that transmits state changes from long-running Rust tasks directly to the renderer process.

Backend Event Emission in Rust

The progress mechanism originates in src-tauri/src/operation.rs, where the backend defines a serializable data structure and emits events through Tauri's event system.

The ProgressUpdate Struct

The payload structure carries all necessary state for UI rendering:

use serde::Serialize;

#[derive(Serialize)]
struct ProgressUpdate {
    step: u32,
    total_steps: u32,
    percent: f32,
    message: Option<String>,
}

This struct is instantiated within asynchronous operations to capture the current iteration, total workload, calculated percentage, and optional status text.

Emitting Events with app.emit_all

During execution of long-running tasks, the backend broadcasts updates using app.emit_all. This method transmits the payload to all connected frontend listeners on the specified channel:

use tauri::{Manager, Runtime};

pub async fn run_long_task<R: Runtime>(app: tauri::AppHandle<R>) {
    let total = 10u32;
    for i in 1..=total {
        // Perform device I/O or processing work here
        
        let payload = ProgressUpdate {
            step: i,
            total_steps: total,
            percent: (i as f32 / total as f32) * 100.0,
            message: Some(format!("Processing step {}", i)),
        };
        
        // Broadcast to all frontend windows
        app.emit_all("progress", payload).unwrap();
        
        // Yield control to prevent blocking
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }
}

The emit_all call targets the "progress" channel, which the frontend monitors. Because the operation runs inside tauri::async_runtime::spawn (invoked via a Tauri command), the main thread remains unblocked, keeping the application window responsive.

Frontend Event Consumption in React

The frontend implementation in src/App.tsx subscribes to the Rust-emitted events and drives UI components through React state.

Listening for Progress Events

The @tauri-apps/api/event module provides the listen function, which registers a callback for the "progress" channel:

import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';
import { Progress } from 'antd';

interface ProgressUpdate {
  step: number;
  total_steps: number;
  percent: number;
  message?: string;
}

export default function App() {
  const [progress, setProgress] = useState<ProgressUpdate>({
    step: 0,
    total_steps: 0,
    percent: 0,
    message: '',
  });

  useEffect(() => {
    const unlisten = listen<ProgressUpdate>('progress', (event) => {
      setProgress(event.payload);
    });
    
    return () => {
      unlisten.then((f) => f());
    };
  }, []);

  // Render UI...
}

The generic listen<ProgressUpdate> ensures type safety across the Rust-to-TypeScript boundary. The cleanup function removes the listener when the component unmounts, preventing memory leaks.

Rendering Real-Time Feedback

The captured state feeds directly into UI components:

return (
  <div>
    <h2>Current Operation</h2>
    <Progress
      percent={progress.percent}
      status={progress.percent < 100 ? 'active' : 'success'}
      format={() => `${progress.step} of ${progress.total_steps}`}
    />
    <p>{progress.message}</p>
  </div>
);

React batches these state updates efficiently, ensuring smooth progress bar animation even when the backend emits events rapidly.

Operation Flow and Threading

The complete lifecycle of a progress-tracked operation follows this sequence:

  1. Invocation: The frontend calls invoke('run_long_task'), which is registered in src-tauri/src/lib.rs as a Tauri command.
  2. Async Execution: The Rust handler spawns the operation on Tauri's async runtime using tauri::async_runtime::spawn, isolating heavy I/O from the main thread.
  3. Periodic Emission: As the task advances, it calculates completion percentages and calls app.emit_all("progress", payload).
  4. Instant Delivery: Tauri's event system serializes the payload and transmits it to the renderer process via WebSocket-style messaging.
  5. UI Reaction: The frontend listener receives the payload, updates React state, and triggers a re-render of the progress indicators.

This architecture ensures that users receive millisecond-level feedback on operation status without the latency or resource overhead of polling mechanisms.

Summary

  • iloader implements real-time progress updates through Tauri's bidirectional event system, connecting Rust backend operations to React frontend state.
  • The ProgressUpdate struct in src-tauri/src/operation.rs standardizes payload structure with fields for step, total_steps, percent, and message.
  • Backend code uses app.emit_all("progress", payload) to broadcast updates from asynchronous tasks spawned via tauri::async_runtime::spawn.
  • The frontend consumes events using listen<ProgressUpdate>('progress', ...) from @tauri-apps/api/event, updating React state to drive progress bars and status text.
  • This event-driven approach eliminates UI blocking and provides instantaneous visual feedback during device operations.

Frequently Asked Questions

How does iloader prevent the UI from freezing during long operations?

iloader executes all heavy processing within tauri::async_runtime::spawn in the Rust backend. This spawns the work onto a separate thread pool, while the main thread continues running the Tauri event loop. The frontend remains responsive because the Rust code never blocks the main thread, and progress updates arrive asynchronously through the event bridge.

What specific data fields does iloader transmit for each progress update?

According to the source code in src-tauri/src/operation.rs, each payload contains a step (current iteration), total_steps (overall count), percent (calculated completion percentage as f32), and an optional message string for human-readable status text. These fields enable both precise progress bars and descriptive status labels in the UI.

Which Tauri API method transmits progress data from Rust to the frontend?

The backend uses app.emit_all("progress", payload) where app is a tauri::AppHandle. This method broadcasts the serialized ProgressUpdate struct to all active frontend windows listening on the "progress" channel. The frontend receives this through the listen function imported from @tauri-apps/api/event.

Can iloader track multiple concurrent operations with separate progress bars?

While the provided source demonstrates a single "progress" channel, the architecture supports expansion by emitting to dynamically named channels (e.g., format!("progress-{}", operation_id)) or by including an operation identifier within the ProgressUpdate struct. The current implementation focuses on sequential operations, but the event system itself supports concurrent event streams without cross-contamination when channels are properly namespaced.

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 →