Reactive Programming in Rust: Libraries, Examples, and Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Reactive Programming in Rust: From Observable Objects to Asynchronous Streams



Reactive programming is a paradigm focused on processing asynchronous data streams and propagating changes. In the Rust ecosystem, it is implemented through a combination of a powerful type system, safe memory management, and modern asynchronous primitives. In this article, we will explore key libraries, patterns, and practical examples of reactive programming in Rust.



Core Libraries for Reactive Programming in Rust



Rust does not have a single "standard" for reactive programming, but there are several mature libraries covering different levels of abstraction:



  • RxRust — a port of the popular ReactiveX (Rx) for Rust. Provides Observable, Observer, Scheduler, and a wide set of operators (map, filter, flat_map, merge, etc.).
  • futures-rs — a fundamental library for working with asynchronous computations. The Stream trait is the foundation of reactive streams.
  • async-std and tokio — asynchronous runtimes providing channels, timers, and other primitives for reactive chains.
  • frunk — a library for functional programming, including reactive patterns like Lens and State.


Example 1: Simple Observable with RxRust



RxRust implements the "observer" pattern: an Observable generates events, and an Observer subscribes to them.



// Cargo.toml: rxrust = "1.0"use rxrust::prelude::*;

fn main() { let observable = observable::from_iter(vec![1, 2, 3, 4, 5]);

observable .filter(|x| x % 2 == 0) .map(|x| x * 10) .subscribe(|x| println!("Received: {}", x));}


In this example, a stream of numbers 1..5 is created, even numbers are filtered, multiplied by 10, and printed. The operator chain is executed lazily — only after calling subscribe.



Example 2: Asynchronous Streams with futures-rs



The futures-rs library provides the Stream trait, which is an asynchronous counterpart to Iterator. Combinators from StreamExt allow building reactive chains.



// Cargo.toml: futures = "0.3"use futures::stream::{self, StreamExt};

#[tokio::main]async fn main() { let stream = stream::iter(0..10) .filter(|x| futures::future::ready(x % 2 == 0)) .map(|x| x * 10) .take(3);

let result: Vec<i32> = stream.collect().await; println!("Result: {:?}", result); // [0, 20, 40]}


Here, filter accepts an asynchronous function, allowing calls to external APIs or databases to be embedded within the chain. take(3) limits the number of elements — this is an example of backpressure.



Example 3: Reactive Channels with async-std



Channels are a classic way to implement reactive interactions between components. async-std provides multi-producer, multi-consumer channels.



// Cargo.toml: async-std = "1.12"use async_std::channel;use async_std::task;

#[async_std::main]async fn main() { let (sender, receiver) = channel::unbounded();

// Producer task::spawn(async move { for i in 0..5 { sender.send(i).await.unwrap(); task::sleep(std::time::Duration::from_millis(100)).await; } });

// Consumer (reactive) while let Some(msg) = receiver.recv().await { println!("Received: {}", msg); if msg == 4 { break; } }}


Channels are ideal for implementing an Event Bus, event bus, or simple reactive systems with multiple data sources.



Advanced Patterns: Combining Streams

Recommendations