# How to Subscribe to iLoader Operation Events in a Custom React Component

> Learn how to subscribe to iLoader operation events in React. Use Tauri's listen function and useEffect hook to handle operation updates in custom components.

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

---

**Use Tauri's `listen` function from `@tauri-apps/api/event` to subscribe to the `"operation"` channel and handle `OperationUpdate` payloads in your component's `useEffect` hook.**

iLoader drives long-running tasks through a centralized operation system that emits Tauri events from the Rust backend. When an operation changes state, any custom React component can subscribe to these updates independently of the global store. This guide shows you how to implement event listeners in nab138/iloader using the same patterns found in the main application.

## Understanding the Event Architecture

iLoader's operation system broadcasts typed events that flow from the backend to the frontend through a dedicated channel.

### The OperationUpdate Type

The **OperationUpdate** interface is defined in [[`src/components/operations.ts`](https://github.com/nab138/iloader/blob/main/src/components/operations.ts)](/blob/main/src/components/operations.ts). This type describes the structure of every event payload:

```typescript
interface OperationUpdate {
  updateType: "started" | "finished" | "failed";
  stepId: string;
  extraDetails?: AppError; // Present only when updateType is "failed"
}

```

The **`stepId`** identifies specific phases like `download`, `install`, or `pair`. The **`updateType`** indicates the lifecycle stage, while **`extraDetails`** contains error information when operations fail.

### Global Listener in App.tsx

The main application registers a global listener in [[`src/App.tsx`](https://github.com/nab138/iloader/blob/main/src/App.tsx)](/blob/main/src/App.tsx) to capture all operation events:

```typescript
import { listen } from "@tauri-apps/api/event";
import type { OperationUpdate } from "./components/operations";

useEffect(() => {
  const unlisten = await listen<OperationUpdate>("operation", (e) => {
    // Global state update logic
  });
  return () => unlisten();
}, []);

```

This establishes the `"operation"` event channel that custom components can tap into.

## Subscribing to Operation Events

Any React component can subscribe directly to operation events without modifying the global store.

### Basic Listener Implementation

Import **`listen`** from `@tauri-apps/api/event` and the **`OperationUpdate`** type. Register the listener inside a `useEffect` hook:

```typescript
import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import type { OperationUpdate } from "../components/operations";

export const useOperationListener = () => {
  const [lastUpdate, setLastUpdate] = useState<OperationUpdate | null>(null);

  useEffect(() => {
    let unlistenFn: (() => void) | undefined;

    const setupListener = async () => {
      unlistenFn = await listen<OperationUpdate>("operation", (event) => {
        setLastUpdate(event.payload);
      });
    };

    setupListener();

    return () => {
      unlistenFn?.();
    };
  }, []);

  return lastUpdate;
};

```

The cleanup function invokes the **`unlisten`** callback to prevent memory leaks when the component unmounts.

### Handling Payload Variations

Operation events emit three distinct update types. Discriminate based on the `updateType` field:

```typescript
await listen<OperationUpdate>("operation", (event) => {
  const { updateType, stepId, extraDetails } = event.payload;
  
  switch (updateType) {
    case "started":
      console.log(`Step ${stepId} began`);
      break;
    case "finished":
      console.log(`Step ${stepId} completed`);
      break;
    case "failed":
      console.error(`Step ${stepId} failed:`, extraDetails);
      break;
  }
});

```

## Integration with Global State

While direct subscription works for isolated components, most applications should consume operation state through the centralized store provided by [[`src/StoreContext.tsx`](https://github.com/nab138/iloader/blob/main/src/StoreContext.tsx)](/blob/main/src/StoreContext.tsx).

Access the store using the context hook:

```typescript
import { useStore } from "../StoreContext";

const Component = () => {
  const { operations } = useStore();
  // Access current operation state without manual event handling
};

```

## Complete Example: OperationWatcher Component

Here is a reusable component that displays the most recent operation update, following the pattern used in [[`src/components/OperationView.tsx`](https://github.com/nab138/iloader/blob/main/src/components/OperationView.tsx)](/blob/main/src/components/OperationView.tsx):

```tsx
import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import type { OperationUpdate } from "../components/operations";

export const OperationWatcher = () => {
  const [lastUpdate, setLastUpdate] = useState<OperationUpdate | null>(null);

  useEffect(() => {
    let unlisten: (() => void) | undefined;

    const startListening = async () => {
      unlisten = await listen<OperationUpdate>("operation", (event) => {
        console.log("Received operation update:", event.payload);
        setLastUpdate(event.payload);
      });
    };

    startListening();

    return () => {
      unlisten?.();
    };
  }, []);

  return (
    <div className="operation-monitor">
      <h3>Current Operation Status</h3>
      {lastUpdate ? (
        <div>
          <p>Step: {lastUpdate.stepId}</p>
          <p>Status: {lastUpdate.updateType}</p>
          {lastUpdate.extraDetails && (
            <pre>{JSON.stringify(lastUpdate.extraDetails, null, 2)}</pre>
          )}
        </div>
      ) : (
        <p>Waiting for operation events...</p>
      )}
    </div>
  );
};

```

## Summary

- **Import `listen`** from `@tauri-apps/api/event` to register operation event handlers in custom components.
- **Use `OperationUpdate`** from [[`src/components/operations.ts`](https://github.com/nab138/iloader/blob/main/src/components/operations.ts)](/blob/main/src/components/operations.ts) for TypeScript type safety when handling payloads.
- **Subscribe to the `"operation"`** event channel to receive real-time updates on task lifecycle changes.
- **Always cleanup listeners** by invoking the unsubscribe function returned by `listen` in your `useEffect` cleanup phase.
- **Prefer the global store** in [[`src/StoreContext.tsx`](https://github.com/nab138/iloader/blob/main/src/StoreContext.tsx)](/blob/main/src/StoreContext.tsx) for state shared across multiple components rather than duplicating listeners.

## Frequently Asked Questions

### What is the exact event name for operation updates in iLoader?

The event channel is named **`"operation"`**. This string is passed as the first argument to Tauri's `listen` function, as implemented in [[`src/App.tsx`](https://github.com/nab138/iloader/blob/main/src/App.tsx)](/blob/main/src/App.tsx). Listening to any other channel will not capture operation lifecycle events.

### How do I properly type the operation event payload?

Import the **`OperationUpdate`** interface from [[`src/components/operations.ts`](https://github.com/nab138/iloader/blob/main/src/components/operations.ts)](/blob/main/src/components/operations.ts) and pass it as the generic parameter to `listen<OperationUpdate>()`. This ensures compile-time checking for `updateType`, `stepId`, and `extraDetails`.

### Should I subscribe to events directly or use the global store?

For **isolated components** like progress indicators, direct subscription is appropriate. For **shared state** accessed by multiple components, consume the store via [[`src/StoreContext.tsx`](https://github.com/nab138/iloader/blob/main/src/StoreContext.tsx)](/blob/main/src/StoreContext.tsx) instead to avoid performance overhead from duplicate listeners.

### What happens if I forget to cleanup the event listener?

Failing to call the unsubscribe function creates a **memory leak**. The listener retains references to your component's state after unmounting. Always return a cleanup function from `useEffect` that invokes the unlisten handler returned by `listen`.