A modern, reactive TUI framework for Rust with React-inspired hooks and components
Features • Quick Start • Examples • Documentation • Contributing
Reratui brings React's powerful component model and hooks system to terminal user interfaces in Rust. Built on top of ratatui, it provides a familiar, declarative approach to building complex TUI applications with proper state management, effects, and async support.
🎯 React-like Component Model
- Implement the
Componenttrait to create reusable components - Compose components naturally with Rust's type system
- Props as struct fields with full type safety
🔄 Fiber Architecture
- Efficient reconciliation and rendering pipeline
- Batched state updates for optimal performance
- Dirty tracking to minimize re-renders
🪝 Comprehensive Hooks System
use_state- Local state with batched updatesuse_effect- Side effects with cleanupuse_context- Share data across component treeuse_memo/use_callback- Memoizationuse_reducer- Complex state managementuse_ref- Mutable references without re-renders
⚡ Async First
use_future- Track async task stateuse_query- Data fetching with caching & retryuse_mutation- Mutation state trackinguse_async_effect- Async side effects
🎮 Rich Event Handling
use_keyboard/use_keyboard_shortcutuse_mouse/use_mouse_hover/use_mouse_draguse_resize/use_media_query
⏱️ Timing & Utilities
use_timeout/use_intervaluse_history- Undo/redo supportuse_form- Form validationuse_id- Unique IDs
Add Reratui to your Cargo.toml:
[dependencies]
reratui = "0.2.1"
tokio = { version = "1.49", features = ["full"] }Create your first component:
use reratui::prelude::*;
struct Counter;
impl Component for Counter {
fn render(&self, area: Rect, buffer: &mut Buffer) {
// State persists across renders
let (count, set_count) = use_state(|| 0);
// Handle keyboard input
use_keyboard_press(move |key| {
match key.code {
KeyCode::Up => set_count.update(|c| c + 1),
KeyCode::Down => set_count.update(|c| c.saturating_sub(1)),
KeyCode::Char('q') => request_exit(),
_ => {}
}
});
// Render UI using ratatui widgets
let block = Block::default()
.title("Counter")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan));
Paragraph::new(format!("Count: {}", count))
.block(block)
.alignment(Alignment::Center)
.render(area, buffer);
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
render(|| Counter).await?;
Ok(())
}The repository includes several examples demonstrating various features:
| Example | Description | Run Command |
|---|---|---|
counter_fiber |
Basic state management | cargo run -p counter-fiber |
counter |
Simple counter (legacy) | cargo run -p counter |
effect_timing |
Effect lifecycle | cargo run -p effect_timing |
async_fetch_example |
Async data fetching | cargo run --example async_fetch_example |
query_example |
Data queries with caching | cargo run --example query_example |
mutation_example |
CRUD operations | cargo run --example mutation_example |
events_showcase |
Keyboard & mouse events | cargo run --example events_showcase |
command_palette |
Complex multi-component app | cargo run --example command_palette |
data_fetcher |
Multiple async sources | cargo run --example data_fetcher |
ink_like_counter |
Ink-style component syntax | cargo run --example ink_like_counter |
use reratui::prelude::*;
use reratui::hooks::{use_query, QueryOptions, QueryStatus};
struct UserList;
impl Component for UserList {
fn render(&self, area: Rect, buffer: &mut Buffer) {
let query = use_query(
"users",
|| async { fetch_users().await },
Some(QueryOptions {
stale_time: Duration::from_secs(30),
retry: true,
retry_attempts: 3,
..Default::default()
}),
);
match query.status {
QueryStatus::Loading => {
Paragraph::new("Loading...").render(area, buffer);
}
QueryStatus::Success => {
if let Some(users) = &query.data {
render_user_list(users, area, buffer);
}
}
QueryStatus::Error => {
if let Some(err) = &query.error {
Paragraph::new(format!("Error: {}", err))
.style(Style::default().fg(Color::Red))
.render(area, buffer);
}
}
_ => {}
}
}
}// Handle specific key combinations
use_keyboard_shortcut(
KeyCode::Char('s'),
KeyModifiers::CONTROL,
|| save_document(),
);
// Handle all key presses
use_keyboard_press(move |key| {
match key.code {
KeyCode::Up => navigate_up(),
KeyCode::Down => navigate_down(),
KeyCode::Enter => select_item(),
_ => {}
}
});// Track hover state
let button_area = Rect::new(10, 5, 20, 3);
let is_hovering = use_mouse_hover(button_area);
// Handle clicks
use_mouse_click(move |button, x, y| {
if button == MouseButton::Left && button_area.contains((x, y).into()) {
handle_button_click();
}
});
// Track drag operations
let (drag_info, reset_drag) = use_mouse_drag();
if drag_info.is_dragging {
// Handle drag...
}Reratui uses a 5-phase render pipeline:
┌─────────────────────────────────────────────────────────┐
│ Render Loop │
│ │
│ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │
│ │ Poll │ → │ Render │ → │ Commit │ → │ Event │ │
│ └──────┘ └────────┘ └────────┘ └──────────┘ │
│ ↑ │ │
│ │ ┌────────┐ │ │
│ └─────────│ Effect │←────────────────────┘ │
│ └────────┘ │
└─────────────────────────────────────────────────────────┘
- Poll - Wait for terminal events or scheduled updates
- Render - Execute component render functions
- Commit - Apply batched state updates
- Event - Process terminal events
- Effect - Run effects and cleanup functions
| Document | Description |
|---|---|
| Hooks Reference | Complete API reference for all hooks |
| Component Guide | How to create and compose components |
| Async Patterns | Data fetching, queries, and mutations |
| Architecture | Internal fiber system and render pipeline |
| Examples Guide | Walkthrough of all examples |
| Migration Guide | Upgrading from older versions |
| React Differences | React vs Reratui comparison |
| Strict Mode | Development mode for catching issues |
let (value, set_value) = use_state(|| initial);
let (state, dispatch) = use_reducer(reducer, initial);
let ref_handle = use_ref(|| initial);
let history = use_history(|| initial);use_effect(|| { /* effect */ Some(Box::new(|| { /* cleanup */ })) }, deps);
use_effect_once(|| { /* runs once on mount */ None });
use_async_effect(|| async { /* async effect */ None }, deps);use_context_provider(|| value);
let value = use_context::<T>();
let maybe_value = try_use_context::<T>();let future = use_future(|| async { Ok(data) }, Some(deps));
let query = use_query("key", || async { fetch() }, options);
let mutation = use_mutation(|args| async { mutate(args) }, options);if let Some(event) = use_event() { /* handle */ }
use_keyboard_press(|key| { /* handle */ });
use_keyboard_shortcut(KeyCode::Char('s'), KeyModifiers::CONTROL, || {});
use_mouse_click(|button, x, y| { /* handle */ });
let is_hovering = use_mouse_hover(area);let timeout = use_timeout(|| { /* callback */ }, delay_ms);
let interval = use_interval(|| { /* callback */ }, interval_ms);let area = use_area();
let frame = use_frame();
let (width, height) = use_resize();
let is_narrow = use_media_query(|(w, _)| w < 80);- Rust 1.85.0 or later (edition 2024)
- Tokio runtime for async support
reratui/
├── crates/
│ ├── reratui/ # Main library
│ │ ├── src/
│ │ │ ├── hooks/ # All hook implementations
│ │ │ ├── scheduler/ # State batching & effects
│ │ │ ├── fiber.rs # Fiber node
│ │ │ ├── fiber_tree.rs
│ │ │ ├── runtime.rs # Main render loop
│ │ │ └── ...
│ │ └── docs/ # Detailed documentation
│ └── reratui-macro/ # Procedural macros
├── examples/ # Example applications
└── ...
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
# Run tests
cargo test
# Run a specific example
cargo run -p counter-fiber
# Check formatting
cargo fmt --check
# Run clippy
cargo clippyLicensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
- ratatui - The excellent TUI library this is built upon
- React - Inspiration for the component model and hooks system
- Ink - Inspiration for bringing React patterns to terminals
Made with ❤️ for the Rust TUI community
