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

> Discover how iloader delivers real-time progress updates for long-running operations using Tauri's event system to stream Rust data to React for instant UI feedback.

- Repository: [Nicholas Sharp/iloader](https://github.com/nab138/iloader)
- Tags: how-to-guide
- Published: 2026-09-13

---

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

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

```rust
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`](https://github.com/nab138/iloader/blob/main/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:

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

```tsx
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`](https://github.com/nab138/iloader/blob/main/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`](https://github.com/nab138/iloader/blob/main/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`](https://github.com/nab138/iloader/blob/main/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.