DEV Community

tengxgfyrz67s
tengxgfyrz67s

Posted on

Keep-Alive-Pattern

Keep Alive Pattern in euv

Project Code:https://github.com/euv-dev/euv

euv is a Rust + WASM frontend UI framework that uses a reactive signal system to manage UI state. When building tab-based interfaces or multi-view applications, a common challenge is preserving the state of a component when the user navigates away and comes back. The Keep Alive pattern solves this by hiding components with CSS display: none instead of unmounting them, ensuring that all internal state — including signals, timers, and form inputs — is preserved across view switches.

In this article, we will explore the Keep Alive pattern in depth, covering CSS display toggling, cleanup hooks for resource management, timer lifecycle management with watch!, and practical scenarios where this pattern shines.

Table of Contents

  1. The Problem: State Loss on Component Unmount
  2. CSS Display Toggle: The Core Technique
  3. Managing Timers with use_cleanup and watch!
  4. When to Use the Keep Alive Pattern
  5. Best Practices and Trade-offs
  6. Conclusion

The Problem: State Loss on Component Unmount

In a typical tab-based interface, switching tabs means unmounting the current tab's component and mounting the new one. This works well for simple cases, but it has a significant drawback: all local state is lost. Consider a tab that contains:

  • A counter that the user has been incrementing
  • A form with partially filled fields
  • A timer running in the background
  • Scroll position within the tab content

When the user switches away and comes back, all of this state is gone. The counter resets to zero, the form fields are empty, the timer has stopped, and the scroll position is back at the top.

The Keep Alive pattern addresses this by keeping the component mounted in the DOM but hidden with display: none, so all state persists across tab switches.

CSS Display Toggle: The Core Technique

The core of the Keep Alive pattern is using CSS display to show and hide tab content instead of conditionally mounting and unmounting components.

let tab: Signal<String> = use_signal(|| "counter".to_string());

html! {
    div {
        div { class: c_tab_bar()
            div { class: if { tab.get() == "counter" } { c_tab_item_active() } else { c_tab_item_inactive() }
                onclick: move |_event: Event| { tab.set("counter".to_string()); } "Counter" }
            div { class: if { tab.get() == "form" } { c_tab_item_active() } else { c_tab_item_inactive() }
                onclick: move |_event: Event| { tab.set("form".to_string()); } "Form" }
        }
        div { style: { display: if { tab.get() == "counter" } { "block".to_string() } else { "none".to_string() }; }
            { counter_tab() } }
        div { style: { display: if { tab.get() == "form" } { "block".to_string() } else { "none".to_string() }; }
            { form_tab() } }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down this implementation:

  1. Tab Bar: Two tab items ("Counter" and "Form") whose active/inactive classes are conditionally set based on the tab signal. Clicking a tab updates the signal.

  2. Counter Tab Content: The counter_tab() component is wrapped in a div whose display style is conditionally set. When tab is "counter", the display is "block" (visible); otherwise, it is "none" (hidden).

  3. Form Tab Content: Similarly, the form_tab() component is wrapped in a div that is visible only when tab is "form".

The key insight is that both counter_tab() and form_tab() remain mounted in the DOM at all times. When a tab is hidden with display: none, it is not removed from the DOM — it simply becomes invisible. This means all signals, event handlers, and internal state within these components are preserved.

When the user switches back to a previously visited tab, the component is instantly visible again with all its state intact. There is no re-render cost, no state re-initialization, and no loss of user data.

Managing Timers with use_cleanup and watch!

One of the most important aspects of the Keep Alive pattern is managing resources like timers. When a tab is hidden, you may want to pause or stop background timers to save resources. When the tab becomes visible again, you may want to restart them. euv provides use_cleanup and watch! to handle this elegantly.

fn timer_tab() -> VirtualNode {
    let elapsed: Signal<i32> = use_signal(|| 0);
    let running: Signal<bool> = use_signal(|| false);
    let handle: Signal<Option<IntervalHandle>> = use_signal(|| None);

    use_cleanup(move || {
        if let Some(h) = handle.get() {
            h.clear();
        }
    });

    watch!(running, |is_running: bool| {
        if is_running {
            let new_handle = use_interval(1000, move || {
                let current: i32 = elapsed_signal.get();
                elapsed_signal.set(current + 1);
            });
            handle_signal.set(Some(new_handle));
        } else {
            if let Some(existing_handle) = handle.get() {
                existing_handle.clear();
            }
            handle.set(None);
        }
    });

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Let's examine this code in detail:

Signals

  • elapsed: Signal<i32> — Tracks the number of seconds elapsed, initialized to 0.
  • running: Signal<bool> — Controls whether the timer is currently running, initialized to false.
  • handle: Signal<Option<IntervalHandle>> — Stores the handle returned by use_interval, or None if no timer is active.

use_cleanup for Resource Management

The use_cleanup hook registers a cleanup function that runs when the component is unmounted. In the Keep Alive pattern, the component is rarely unmounted (since it stays in the DOM), but use_cleanup is still essential as a safety net.

use_cleanup(move || {
    if let Some(h) = handle.get() {
        h.clear();
    }
});
Enter fullscreen mode Exit fullscreen mode

This ensures that if the component is ever unmounted (e.g., when the parent component is destroyed), the interval timer is properly cleared to prevent memory leaks.

watch! for Reactive Timer Control

The watch! macro observes the running signal and reacts to its changes. This is where the timer lifecycle management happens:

watch!(running, |is_running: bool| {
    if is_running {
        let new_handle = use_interval(1000, move || {
            let current: i32 = elapsed_signal.get();
            elapsed_signal.set(current + 1);
        });
        handle_signal.set(Some(new_handle));
    } else {
        if let Some(existing_handle) = handle.get() {
            existing_handle.clear();
        }
        handle.set(None);
    }
});
Enter fullscreen mode Exit fullscreen mode

When running becomes true:

  1. A new interval timer is created with use_interval(1000, ...), which fires every 1000 milliseconds (1 second).
  2. Each time the interval fires, it reads the current elapsed value and increments it by 1.
  3. The interval handle is stored in the handle signal for later cleanup.

When running becomes false:

  1. If there is an existing interval handle, it is cleared (stopped).
  2. The handle signal is set to None.

This pattern allows you to start and stop the timer reactively based on any condition — not just tab visibility, but also user actions (e.g., a "Start"/"Pause" button), component lifecycle events, or any other signal change.

Combining Keep Alive with Timer Management

In a real application, you might combine the Keep Alive pattern with timer management by tying the running signal to the tab's visibility:

// Inside a Keep Alive tab component:
// watch!(tab, |current_tab: String| {
//     if current_tab == "timer" {
//         running.set(true);
//     } else {
//         running.set(false);
//     }
// });
Enter fullscreen mode Exit fullscreen mode

This ensures the timer only runs when the tab is visible, saving CPU cycles when the tab is hidden.

When to Use the Keep Alive Pattern

The Keep Alive pattern is ideal for the following scenarios:

1. Tab Interfaces with Stateful Components

When your application has multiple tabs and each tab contains stateful components (counters, forms, lists with user interactions), the Keep Alive pattern ensures that users don't lose their work when switching between tabs.

2. Multi-Step Forms

In a multi-step form where each step is a separate "view", Keep Alive preserves the data entered in previous steps when the user navigates back and forth.

3. Dashboards with Live Data

Dashboards that display real-time data (charts, metrics, activity feeds) benefit from Keep Alive because the data doesn't need to be reloaded each time the user returns to that view.

4. Media Players

If your application includes a media player (audio or video), Keep Alive ensures that playback continues (or at least the player state is preserved) when the user navigates to other parts of the application.

5. Complex Interactive Components

Components like code editors, drawing canvases, or data tables with user-configurable views benefit greatly from Keep Alive because they tend to have significant internal state.

Best Practices and Trade-offs

Best Practices

  1. Always use use_cleanup for resource management: Even though Keep Alive components are rarely unmounted, always register cleanup functions for timers, event listeners, and other resources.

  2. Use watch! to pause/resume background tasks: When a tab is hidden, pause any background timers or animations to save resources.

  3. Limit the number of Keep Alive components: Each hidden component still consumes memory and maintains its signal graph. Don't use Keep Alive for components that are rarely revisited.

  4. Consider lazy initialization: If a tab's content is expensive to initialize, consider deferring the initialization until the first time the tab is visited, while still keeping it alive afterward.

  5. Test for memory leaks: Since Keep Alive components persist for the lifetime of the parent, make sure they don't accumulate unbounded data over time.

Trade-offs

  • Memory usage: Keep Alive components remain in memory even when hidden. For applications with many tabs or complex components, this can increase memory consumption.
  • CPU usage: Signal watchers and computed values in hidden components may still execute. Use watch! to pause unnecessary work when a tab is hidden.
  • Complexity: The Keep Alive pattern adds complexity to your component architecture. Use it only when state preservation is a genuine requirement.

Conclusion

The Keep Alive pattern is a powerful technique in euv for preserving component state across view switches. By using CSS display: none to hide components instead of unmounting them, you ensure that all reactive signals, timers, and user data remain intact. Combined with use_cleanup for resource cleanup and watch! for reactive timer management, you can build tab-based interfaces that provide a seamless, stateful user experience.

The key to using Keep Alive effectively is understanding when it's needed (stateful tabs, multi-step forms, dashboards) and when it's not (simple, stateless views). By following the best practices outlined in this article, you can leverage the Keep Alive pattern to build polished, professional web applications with euv.


Project Code:https://github.com/euv-dev/euv

Top comments (0)