Streams
Background data sources that update the UI automatically over time.
let counter = stream!(cx, || {
(0..).map(|i| {
std::thread::sleep(Duration::from_secs(1));
i
})
});
View::text(format!("Count: {}", counter.get()))
Run with: cargo run -p telex-tui --example 04_timer
What are streams?
Streams let you integrate background data sources into your UI. Each value the stream yields triggers a re-render with the new data.
Common uses:
- Timers and clocks
- System monitoring (CPU, memory, network)
- Log tailing
- Live data feeds
- Progress tracking
When to Use Streams
Use streams when:
- Data updates continuously over time
- You're polling, monitoring, or tailing
- The data source is external (files, system, network)
Use effects instead when:
- You need to react to state changes
- You're doing one-time initialization
- You need cleanup logic (timers, subscriptions)
Use async instead when:
- One-time data fetch (API call, database query)
- Clear start and end (loading → ready/error)
See Overview for more guidance on choosing the right approach.
Creating a stream
Use the stream! macro with a closure that returns an iterator:
let elapsed = stream!(cx, || {
(0u64..).map(|s| {
std::thread::sleep(Duration::from_secs(1));
s
})
});
The stream runs in a background thread. Each time the iterator yields a value, Telex triggers a re-render.
Reading stream values
Use .get() to read the latest value:
let seconds = elapsed.get();
View::text(format!("Elapsed: {}s", seconds))
.get() returns the most recent value yielded by the stream.
Checking stream status
Use .is_loading() to check if the stream is still active:
let is_running = stream.is_loading();
if is_running {
View::styled_text(" ●").color(Color::Green).build() // active
} else {
View::styled_text(" ○").dim().build() // stopped
}
A stream is "loading" while it's producing values. Once the iterator ends or the component unmounts, is_loading() returns false.
Multiple streams
You can create multiple independent streams:
let cpu = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_millis(500));
get_cpu_usage()
})
});
let memory = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_millis(800));
get_memory_usage()
})
});
let network = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_millis(300));
get_network_traffic()
})
});
Run with: cargo run -p telex-tui --example 08_system_monitor
Each stream runs independently in its own thread. They update at different rates and don't block each other.
Stream lifecycle
Creation: The stream starts when the component first renders and calls stream!.
Updates: Each yielded value triggers a re-render. The UI shows the latest value.
Cleanup: When the component unmounts, the stream is automatically stopped and the background thread is cleaned up.
You don't manually start, stop, or manage stream threads.
Infinite vs finite streams
Infinite streams run until the component unmounts:
let timer = stream!(cx, || {
(0..).map(|i| { // infinite iterator
std::thread::sleep(Duration::from_secs(1));
i
})
});
Finite streams stop after producing all values:
let countdown = stream!(cx, || {
(0..=10).rev().map(|i| { // 10, 9, 8, ..., 0
std::thread::sleep(Duration::from_secs(1));
i
})
});
// After 11 seconds, is_loading() returns false
Reactive updates
Stream values are reactive. Render automatically when they change:
let cpu_val = cpu.get();
let color = if cpu_val > 80 {
Color::Red
} else if cpu_val > 50 {
Color::Yellow
} else {
Color::Green
};
View::styled_text(format!("CPU: {}%", cpu_val))
.color(color)
.build()
Each time cpu yields a new value, the color recalculates and the UI updates.
Error handling
If your stream can fail, yield Result values:
let data = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_secs(1));
fetch_data() // returns Result<T, E>
})
});
match data.get() {
Ok(value) => View::text(format!("Data: {}", value)),
Err(e) => View::styled_text(format!("Error: {}", e)).color(Color::Red).build(),
}
Progress indicators
Combine finite streams with progress bars:
let progress = stream!(cx, || {
(0..=100).map(|i| {
std::thread::sleep(Duration::from_millis(50));
i
})
});
let pct = progress.get();
let is_done = !progress.is_loading();
View::vstack()
.child(View::text(format!("Progress: {}%", pct)))
.child(progress_bar(pct))
.child(if is_done {
View::text("Complete!")
} else {
View::text("Working...")
})
.build()
Real-world patterns
Log viewer:
let logs = stream!(cx, || {
std::fs::File::open("/var/log/app.log")
.and_then(|file| {
std::io::BufReader::new(file)
.lines()
.collect::<Result<Vec<_>, _>>()
})
.unwrap_or_default()
.into_iter()
});
System stats:
let stats = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_secs(2));
SystemStats {
cpu: get_cpu(),
memory: get_memory(),
disk: get_disk(),
}
})
});
Periodic API polling:
let api_data = stream!(cx, || {
(0..).map(|_| {
std::thread::sleep(Duration::from_secs(30));
fetch_from_api()
})
});
Performance considerations
Update frequency: Don't yield faster than the UI can render. Sleeping for at least 100-300ms between yields is reasonable.
Thread overhead: Each stream is a thread. Dozens of streams is fine, hundreds might be excessive.
Data size: Stream values are cloned when accessed with .get(). Keep them reasonably sized, or wrap large data in Rc<T>.
Blocking operations: Stream closures run in background threads, so blocking operations (file I/O, network calls) are fine.
Tips
Sleep on the first iteration - Streams yield their first value immediately. If your stream sleeps 1 second between yields, put the sleep at the start to avoid an instant first value followed by a delay.
Finite streams for progress - If your task has a known endpoint, use a finite iterator and check is_loading() to detect completion.
Channels for external events - If you need to push data from outside (another thread, a library callback), use the channel! macro instead of wrapping a channel in a stream:
let ch = channel!(cx, String);
let tx = ch.tx(); // WakingSender — wakes event loop instantly
// Pass tx to external code, read with ch.get() in render
See Channels & Ports for full details.
Don't use streams for user input - User actions should go through state and callbacks, not streams. Streams are for external data sources.
Next: Channels & Ports