# How to Use Subscriptions for Timers and External Events in the Native SDK Effect System

> Learn to manage timers and external events with Native SDK's effect system subscriptions. Automate cleanup when components unmount and simplify your code.

- Repository: [Vercel Labs/native](https://github.com/vercel-labs/native)
- Tags: how-to-guide
- Published: 2026-07-18

---

**The Native SDK effect system uses Subscription objects returned from component functions to automatically manage timers and external event listeners, ensuring cleanup occurs when components unmount without manual teardown code.**

The vercel-labs/native repository implements a lightweight reactive model where **effect functions** return **Subscription** handles to manage side effects. This pattern eliminates the need for manual cleanup code by delegating timer cancellation and event listener removal to the runtime according to the lifecycle defined in `src/runtime/effects.zig`.

## Core Concepts of the Effect System

In `src/runtime/effects.zig`, the **Subscription** type serves as an opaque handle that wraps a cancel function. When a component function returns a subscription, the runtime places it into the component's effect list and invokes the cancel routine automatically during teardown.

The architecture follows three distinct lifecycle phases:

- **Start** — The runtime invokes the subscription's initialization logic, registering timers or listeners with the platform
- **Update** — Subsequent renders reuse existing subscription objects to prevent duplicate registrations
- **Dispose** — The runtime iterates the subscription list and invokes each cancel function when the component unmounts

This deterministic lifecycle eliminates common bugs like zombie timers or leaked listeners.

## Managing Timers Through Subscriptions

Timer subscriptions reside in `src/runtime/clock.zig` and provide thin wrappers around platform-specific timer facilities (such as `setTimeout` on web or `dispatch_source` on macOS). The SDK exposes two primary constructors: `Timer.start` for one-shot delays and `Timer.interval` for recurring execution.

### One-Shot Timers with Timer.start

To schedule a delayed callback that automatically cleans up, return a `Timer.start` subscription from your component:

```zig
pub fn MyComponent() !void {
    const timer = Timer.start(2 * std.time.ns_per_s, fn () void {
        std.log.info("Timer fired!", .{});
    });

    return timer;
}

```

The runtime automatically cancels the underlying platform timer when `MyComponent` unmounts.

### Recurring Timers with Timer.interval

For repeated execution, use `Timer.interval` which registers a repeating timer with the native platform:

```zig
pub fn RepeatingComponent() !void {
    const interval = Timer.interval(1 * std.time.ns_per_s, fn () void {
        std.log.info("Every second", .{});
    });

    return interval;
}

```

Both helpers ensure that the associated callbacks stop firing once the component is removed from the tree.

## Subscribing to External Events

External event subscriptions follow the same pattern as timers. Platform-specific modules expose factory functions that return Subscription objects encapsulating OS listeners and their cancel routines.

### Clipboard Events

The clipboard implementation, demonstrated in `src/runtime/effects_clipboard_tests.zig` (where the production code mirrors the test helper), provides the `Clipboard.listen` factory:

```zig
pub fn ClipboardWatcher() !void {
    const sub = Clipboard.listen(fn (event: Clipboard.Event) void {
        std.log.info("Clipboard changed: {s}", .{event.text});
    });

    return sub;
}

```

This automatically unsubscribes from platform clipboard notifications when the component teardown occurs.

### Audio and Network Events

Similar patterns exist for hardware and connectivity monitoring. In `src/runtime/effects_audio_tests.zig`, the `Audio.onDeviceChange` factory returns subscriptions for audio hardware changes. Network status monitoring follows an identical pattern through `Network.onChange` or `Network.onStatusChange`.

All external event factories return **Subscription** objects that the runtime manages according to the lifecycle defined in `src/runtime/effects.zig`.

## Subscription Lifecycle and Automatic Cleanup

The runtime management logic, validated in `src/runtime/effects_tests.zig`, governs how subscriptions interact with the component lifecycle.

When an effect function returns a subscription:

1. The SDK places the subscription into the component's effect list
2. The runtime invokes the subscription's `start` method (if present), registering timers or listeners with the underlying platform
3. On subsequent renders, existing subscriptions are reused rather than recreated
4. During unmount, the runtime iterates the subscription list and invokes each cancel function

This mechanism guarantees that resources like `setTimeout` handles on web or `dispatch_source` timers on macOS are properly released without explicit disposal code in user components.

## Summary

- **Effect functions** in the Native SDK return **Subscription** objects to manage side effects programmatically
- **Timer subscriptions** created via `Timer.start` and `Timer.interval` in `src/runtime/clock.zig` handle both one-shot and recurring timers automatically
- **External event subscriptions** from modules like `Clipboard.listen` and `Audio.onDeviceChange` encapsulate platform listeners with automatic cleanup
- The runtime in `src/runtime/effects.zig` manages subscription lifecycle through Start, Update, and Dispose phases, preventing memory leaks and zombie timers
- Unit tests in `src/runtime/effects_tests.zig` validate that all subscription types properly release resources during component teardown

## Frequently Asked Questions

### What happens if a component function doesn't return a subscription?

If a component function returns void or a non-Subscription type, the runtime creates no managed handles for that effect. While this works for pure render functions, any timers or listeners created without returning a subscription will leak resources when the component unmounts, as the runtime cannot track or cancel them.

### How does the Native SDK prevent timer leaks?

The SDK prevents leaks through deterministic cleanup in `src/runtime/effects.zig`. When a component unmounts, the runtime iterates the component's subscription list and invokes each cancel function. For timers created via `Timer.start` or `Timer.interval`, these cancel functions stop the underlying platform timers immediately.

### Can multiple subscriptions be combined in a single component?

While each effect function returns a single value, you can structure your component to manage multiple subscriptions by aggregating them or using a wrapper that returns a composite subscription. However, the standard pattern involves creating separate effect functions or returning a single subscription that manages multiple resources internally, as demonstrated in the test files.

### What's the difference between Timer.start and Timer.interval?

**Timer.start** creates a one-shot subscription that fires a single callback after a specified duration, while **Timer.interval** creates a recurring subscription that repeatedly invokes the callback at the specified interval until the component unmounts. Both are defined in `src/runtime/clock.zig` and return Subscription objects that the runtime automatically cancels during teardown.