Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

useInterval Example

Live Demo

Source Code

#![cfg_attr(target_family = "wasm", no_main)]

use std::cell::RefCell;
use std::time::Duration;

use gpui::{px, size, App, Bounds, WindowBounds, WindowOptions};
use vgui::prelude::*;

#[cfg(not(target_family = "wasm"))]
use gpui_platform::application;

#[cfg(target_family = "wasm")]
use gpui_platform::single_threaded_web;

fn app() -> impl gpui::IntoElement {
    // ── use_interval demo ───────────────────────────────────────────
    // A counter that ticks every `delay` milliseconds. The delay is
    // reactively controlled — changing it re-runs the effect inside
    // `use_interval`, cancelling the old timer and starting a new one.
    // Setting delay to 0 pauses the interval entirely.
    let (count, set_count) = create_signal(0i32);
    let (delay, set_delay) = create_signal(1000u64);
    let (running, set_running) = create_signal(true);

    // When `running` is false, we feed 0 into the delay signal so the
    // interval pauses. When true, we use the user-selected delay.
    let effective_delay = create_memo({
        let delay = delay.clone();
        let running = running.clone();
        move || if running.get() { delay.get() } else { 0 }
    });

    let set_count_reset = set_count.clone();
    use_interval(
        move |cx| set_count.update(cx, |n| *n += 1),
        effective_delay,
    );
    let set_running_start = set_running.clone();
    let set_running_pause = set_running.clone();

    view! {
        <div class="flex flex-col gap-4 p-6 bg-[#1a1a2e] w-full h-full text-white justify-center items-center">
            <h2 class="text-lg font-bold">{"useInterval Demo"}</h2>

            // ── Timer display ────────────────────────────────────────
            <div class="text-4xl font-mono tabular-nums">
                {format!("{}s", count.get() / 10)}
            </div>
            <div class="text-sm text-[#aaa]">
                {format!("ticks: {}", count.get())}
            </div>

            // ── Start / Pause ────────────────────────────────────────
            <div class="flex gap-2">
                <button
                    class={twc!(
                        "px-4 py-2 rounded font-medium",
                        running.get().then_some("bg-[#3b82f6] hover:bg-[#2563eb]"),
                        (!running.get()).then_some("bg-[#666] hover:bg-[#555]")
                    )}
                    on:click={click(move |cx| set_running_start.set(cx, true))}
                >
                    {"Start"}
                </button>
                <button
                    class={twc!(
                        "px-4 py-2 rounded font-medium",
                        (!running.get()).then_some("bg-[#ef4444] hover:bg-[#dc2626]"),
                        running.get().then_some("bg-[#666] hover:bg-[#555]")
                    )}
                    on:click={click(move |cx| set_running_pause.set(cx, false))}
                >
                    {"Pause"}
                </button>
                <button
                    class="px-4 py-2 rounded font-medium bg-[#6366f1] hover:bg-[#4f46e5]"
                    on:click={click(move |cx| {
                        set_count_reset.set(cx, 0);
                    })}
                >
                    {"Reset"}
                </button>
            </div>

            // ── Delay selector ───────────────────────────────────────
            <div class="flex flex-col gap-2 items-center">
                <span class="text-sm text-[#aaa]">{format!("Delay: {} ms", delay.get())}</span>
                <div class="flex gap-2">
                    {vgui::for_each([100u64, 500, 1000, 2000], move |d, _| {
                        let set_delay = set_delay.clone();
                        let delay = delay.clone();
                        view! {
                            <button
                                class={twc!(
                                    "px-3 py-1.5 rounded text-sm",
                                    (delay.get() == d).then_some("bg-[#10b981] text-white"),
                                    (delay.get() != d).then_some("bg-[#333] hover:bg-[#444] text-[#ccc]")
                                )}
                                on:click={click(move |cx| set_delay.set(cx, d))}
                            >
                                {format!("{}ms", d)}
                            </button>
                        }
                    })}
                </div>
            </div>

            // ── set_interval demo (independent) ──────────────────────
            <Blinker />
        </div>
    }
}

/// A self-contained component that uses the low-level `set_interval`
/// directly (without `use_interval`). The interval is created once on
/// first render and cancelled when the scope is disposed via
/// `on_cleanup`.
#[allow(non_snake_case)]
fn Blinker() -> impl gpui::IntoElement {
    let (on, set_on) = create_signal(false);

    // `set_interval` returns an `IntervalHandle` that owns the gpui task.
    // Dropping the handle cancels the interval (like JS `clearInterval`).
    // Here we keep the handle alive by moving it into `on_cleanup`, which
    // runs — and drops it — when this scope is disposed.
    let handle = set_interval(
        move |cx| set_on.update(cx, |v| *v = !*v),
        Duration::from_millis(600),
    );
    let handle = RefCell::new(Some(handle));
    on_cleanup(move || {
        *handle.borrow_mut() = None;
    });

    view! {
        <div class="flex items-center gap-2 mt-2">
            <span class="text-sm text-[#aaa]">{"set_interval blink:"}</span>
            <div class={twc!(
                "w-4 h-4 rounded-full transition-colors",
                on.get().then_some("bg-[#f59e0b]"),
                (!on.get()).then_some("bg-[#333]")
            )} />
        </div>
    }
}

fn run() {
    #[cfg(not(target_family = "wasm"))]
    let gpui_app = application();

    #[cfg(target_family = "wasm")]
    let gpui_app = single_threaded_web();

    let launch = |cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(460.), px(420.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |window, cx| vgui::mount(window, cx, app),
        )
        .unwrap();
    };

    #[cfg(not(target_family = "wasm"))]
    gpui_app.run(launch);

    #[cfg(target_family = "wasm")]
    std::mem::forget(gpui_app.run_embedded(launch));
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    vgui::intercept_keyboard_events();
    run();
}

Key Concepts

set_interval — low-level repeating timer

Analogous to JavaScript’s setInterval. Spawns a gpui foreground task that calls the callback every duration. The callback receives &mut gpui::AsyncApp so it can update signals and entities across await points. The returned IntervalHandle owns the underlying task; dropping it cancels the interval immediately (equivalent to clearInterval).

use_interval — reactive hook

Combines create_effect + on_cleanup to provide a declarative interval that responds to reactive state:

  • Takes a ReadSignal<u64> delay in milliseconds. When the value is 0 the interval is paused.
  • When the delay changes, the effect re-runs: the previous interval is cancelled and a new one starts with the updated period.
  • When the enclosing scope is disposed (e.g. a <Switch> branch goes inactive), on_cleanup cancels the interval automatically.

Pattern: pausing via memo

A common pattern is to derive the effective delay from multiple signals — e.g. a running boolean and a user-selected delay — using create_memo:

#![allow(unused)]
fn main() {
let effective_delay = create_memo(move || {
    if running.get() { delay.get() } else { 0 }
});
use_interval(move |cx| set_count.update(cx, |n| *n += 1), effective_delay);
}

Setting running to false feeds 0 into use_interval, which pauses the interval without dropping the hook.

Running

Native

cargo run -p vgui-use-interval

Web (WASM)

cargo build --target wasm32-unknown-unknown -p vgui-use-interval --release
wasm-bindgen --target web --out-dir examples/use-interval/dist \
    --no-typescript target/wasm32-unknown-unknown/release/use-interval.wasm
python3 scripts/serve_plain.py 8080 examples/use-interval