Telex logo Telex

Rust Patterns That Matter #6: The Combo - Rc<RefCell<T>>

Post 6 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.

Previous: #5: Shared Ownership | Next: #7: Split Borrows

#4 covered RefCell: mutation through a shared reference, checked at runtime. #5 covered Rc: multiple owners of the same data, but read-only. When you need both - shared ownership and mutation - you combine them. This is one of the most common patterns in single-threaded Rust.

The motivation

You're building a simple observer pattern. A subject holds a value. Multiple observers watch the subject and react when it changes. Each observer needs a handle to the subject so it can read (and trigger updates to) the shared value.

use std::rc::Rc;

struct Subject {
    value: i32,
}

fn main() {
    let subject = Rc::new(Subject { value: 0 });

    let observer_a = Rc::clone(&subject);
    let observer_b = Rc::clone(&subject);

    // Both observers can read the value
    println!("{}", observer_a.value);

    // But neither can modify it — Rc gives &T, never &mut T
}

Rc shares the data, but you can't mutate it. And you can't wrap it in RefCell alone because you need multiple owners, not just one with interior mutability. You need both.

The pattern: Rc<RefCell<T>>

Wrap the data in RefCell for mutation, then wrap that in Rc for sharing. Each owner gets a clone of the Rc. To mutate, they call .borrow_mut() on the RefCell inside.

use std::cell::RefCell;
use std::rc::Rc;

struct Subject {
    value: i32,
}

fn main() {
    let subject = Rc::new(RefCell::new(Subject { value: 0 }));

    let observer_a = Rc::clone(&subject);
    let observer_b = Rc::clone(&subject);

    // Observer A increments
    observer_a.borrow_mut().value += 1;

    // Observer B reads the updated value
    println!("{}", observer_b.borrow().value); // 1

    // Observer B also mutates
    observer_b.borrow_mut().value += 10;

    // Observer A sees the change
    println!("{}", observer_a.borrow().value); // 11
}

Rc handles the sharing. RefCell handles the mutation. Together they give you shared mutable state in single-threaded Rust.

"Is this a code smell?"

No. This comes up often enough that it deserves a direct answer. If you search for Rc<RefCell<T>> online, you'll find people calling it an antipattern or saying it "fights the borrow checker." That framing is wrong.

In garbage-collected languages, every heap object is implicitly Rc<RefCell<T>> - shared by reference, mutable by anyone, garbage collected when nobody holds a reference. Rust makes this machinery explicit. The pattern isn't fighting the borrow checker; it's the standard way to express shared mutable state when the borrow checker's static analysis can't model your access pattern.

UI frameworks, observer patterns, graph structures, callback registries - these all have shared mutable state. Rc<RefCell<T>> is how idiomatic Rust handles it.

In practice: Telex's State<T>

Telex wraps this pattern in a clean API. Under the hood, State<T> is an Rc<RefCell<T>> with convenience methods:

pub struct State<T> {
    inner: Rc<StateInner<T>>,
}

// Users write this:
let count = State::new(0);
println!("{}", count.get());      // 0
count.update(|n| *n += 1);
println!("{}", count.get());      // 1

State::new creates the Rc<RefCell<T>>. .get() calls .borrow() and clones the value. .update() calls .borrow_mut() and applies the closure. The Rc<RefCell<T>> is invisible to the component author. See Designing a TUI Framework - Part 1 for the full story.

The common pitfall: overlapping borrows

RefCell panics if you violate the borrow rules at runtime. The most common way to trigger this is holding a .borrow_mut() while something else tries to borrow the same cell.

let data = Rc::new(RefCell::new(vec![1, 2, 3]));

// BAD: borrow_mut lives too long
let mut guard = data.borrow_mut();
guard.push(4);
println!("{}", data.borrow().len()); // PANIC: already mutably borrowed

The fix: scope the mutable borrow so it's dropped before the shared borrow.

let data = Rc::new(RefCell::new(vec![1, 2, 3]));

// GOOD: borrow_mut is scoped
{
    data.borrow_mut().push(4);
} // mutable borrow dropped here

println!("{}", data.borrow().len()); // 4 — works

This is the main discipline Rc<RefCell<T>> requires: keep borrows short-lived. Borrow, do the work, drop the guard. Don't hold guards across function calls or callbacks where another borrow might happen.

When to use it

Good uses:

When not to use it:

See it in practice: Building a Chat Server #2: Rooms and Users uses this pattern for shared mutable membership lists.