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:
- UI component state (exactly what Telex does)
- Observer/subscriber patterns
- Shared mutable caches accessed from multiple components
- Graph nodes that need to be both shared and mutable
When not to use it:
- Across threads - use
Arc<Mutex<T>>(#19) instead - When you can use
&mut selfmethods and pass ownership clearly - the compiler's static checking is always preferable to runtime checking - For performance-critical inner loops where even the overhead of refcounting matters - consider an arena with indices (#8) instead
See it in practice: Building a Chat Server #2: Rooms and Users uses this pattern for shared mutable membership lists.
Telex