<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[out-of-order]]></title><description><![CDATA[out-of-order]]></description><link>https://out-of-order.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 24 Oct 2026 10:51:30 GMT</lastBuildDate><atom:link href="https://out-of-order.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How GPUI's Entity Model Solves Rust State Management (With a Ratatui Example)]]></title><description><![CDATA[Inspired by GPUI's ownership model, I built a Rust crate that manages state via entity handles into a central store—then used it in a ratatui app.]]></description><link>https://out-of-order.hashnode.dev/how-gpui-s-entity-model-solves-rust-state-management-with-a-ratatui-example</link><guid isPermaLink="true">https://out-of-order.hashnode.dev/how-gpui-s-entity-model-solves-rust-state-management-with-a-ratatui-example</guid><category><![CDATA[Rust]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[rust lang]]></category><dc:creator><![CDATA[Mouhamad Al Mounayar]]></dc:creator><pubDate>Tue, 13 Oct 2026 21:50:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acbb58c99473e7a83b7ff75/9542758b-7408-480f-b601-b0641dbab0a0.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Introduction</h2>
<p>Gpui has been gaining a lot of popularity these last couple of months, and I am proud to say I have been a fan of the framework long before that. As a matter of fact, here is a tweet by me saying that people should talk about it more (when people didn't talk about it more):</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acbb58c99473e7a83b7ff75/79723c9a-1bfb-4364-81d6-7c29f5bb4d82.png" alt="" style="display:block;margin:0 auto" />

<p>In my opinion, two things are contributing to the popularity of the framework today:</p>
<ul>
<li><p><strong>Performance:</strong> I think it is clear to everyone now that gpui is the <strong>best</strong> alternative to electron applications.</p>
</li>
<li><p><strong>AI:</strong> Many people hate using Rust because of the borrow checker, but with agents, they no longer have to deal with it as much.</p>
</li>
</ul>
<p>What I really loved about the framework, however, is its approach to state management. When I first started building terminal user interfaces with Rust, I struggled with how I should approach state management, because with Rust (unlike JavaScript) the borrow checker makes things especially hard.</p>
<p>After seeing how gpui handled this, by studying the codebase and reading this <a href="https://zed.dev/blog/gpui-ownership">gem of an article</a>, I noticed that the approach adopted can be transferred to any other application which wants to manage state effectively.</p>
<p>In this blog post, I will share how I built an experimental state management crate for Rust, and I will use it to build an application with ratatui.</p>
<h2>State Management Crate</h2>
<p>Our state management crate creates a state. It provides components of the application with ways to read and update said state. In Rust, each variable has a unique owner. So, in order to avoid the complexity that comes with the borrow checker, the state is going to be owned by one <code>App</code> object.</p>
<p>Instead of multiple components holding references to the state owned by the <code>App</code>, the <code>App</code> gives out cheap handles to the components which want to read or update it. This is possible because the <code>App</code> stores its entities in a <code>SlotMap</code> data structure.</p>
<p>A <code>SlotMap</code> is an array which allows <code>O(1)</code> insertion and retrieval of elements by key. A key is returned after inserting a value, and the key will always refer to the value which was inserted. This makes it ideal for storing our application entities.</p>
<p>For our use case, the <a href="https://docs.rs/slotmap">slotmap</a> crate is used. With that dependency in place, we can start building our crate, beginning with the <code>App</code> struct that owns everything.</p>
<h3>App</h3>
<p>So, let us define the <code>App</code> struct as follows:</p>
<pre><code class="language-rust">pub struct App {
  map: SlotMap&lt;DefaultKey, Option&lt;Box&lt;dyn Any&gt;&gt;&gt;
}
</code></pre>
<ul>
<li><p><code>DefaultKey</code> is the type of the key returned after inserting values into the <code>SlotMap</code>.</p>
</li>
<li><p>Values are <code>Option&lt;Box&lt;dyn Any&gt;&gt;</code>, since a state can be anything; wrapping it in an <code>Option</code> is useful for preventing reentrant updates.</p>
</li>
</ul>
<h3>Entity</h3>
<p>Next, let's define what an <code>Entity</code> is. In gpui, <code>Entity</code> represents the handle that the <code>App</code> gives out. It basically contains the key of the value inserted into the <code>slotmap</code>, a <code>ref_count</code> to count how many times the entity was cloned, and a <code>PhantomData</code> which specifies which type the <code>Entity</code> holds.</p>
<pre><code class="language-rust">pub struct Entity&lt;T&gt; {
  entity_id: DefaultKey,
  ref_count: Rc&lt;()&gt;,
  _marker: PhantomData&lt;T&gt; 
}
</code></pre>
<p>With this, we can implement a method to register new entities.</p>
<pre><code class="language-rust">impl App {
  fn new&lt;T: 'static&gt;(&amp;mut self, build: impl FnOnce(&amp;App) -&gt; T) -&gt; Entity&lt;T&gt; {
    let state = build(self);
    let entity_id = self.map.insert(Some(Box::new(state)));
    Entity {
      entity_id,
      ref_count: Rc::new(()),
      _marker: PhantomData
    }
  }
}
</code></pre>
<p>As you can see, the method accepts a closure which is handed a reference to the <code>App</code>. This is because some entities might need to be initialised with data from other entities in <code>App</code>.</p>
<p>Let us now implement a method to read from a state.</p>
<pre><code class="language-rust">impl App {
    pub fn read&lt;T: 'static&gt;(&amp;self, entity: &amp;Entity&lt;T&gt;) -&gt; &amp;T {
        let value = self.map.get(entity.entity_id).expect("stale handle");
        match value {
            Some(value) =&gt; value.downcast_ref::&lt;T&gt;().unwrap(),
            None =&gt; {
                panic!("cannot read a value which is currently being updated")
            }
        }
    }
}
</code></pre>
<p><code>SlotMap::get</code> returns <code>None</code> when the key isn't present in the map. This happens if the entity was removed (and the slot's generation bumped), or if the handle is simply invalid. We <code>expect</code> here rather than propagate the error, because reaching this state should never happen in a correctly-behaving program. If it does, it signals a bug in our architecture, not a recoverable condition. Since this is an experimental crate, we're choosing to fail loudly rather than handle every edge case gracefully.</p>
<p>The next step is to implement an <code>update</code> method in order to update entities.</p>
<pre><code class="language-rust">impl App {
    pub fn update&lt;T: 'static, R&gt;(
        &amp;mut self,
        entity: Entity&lt;T&gt;,
        update: impl FnOnce(&amp;mut T, &amp;mut App) -&gt; R,
    ) -&gt; R {
        let value = self.map.get_mut(entity.entity_id).expect("stale handle");
        let boxed = value.take().expect("entity already being updated");
        let mut state: T = *boxed.downcast::&lt;T&gt;().unwrap();
        let result: R = update(&amp;mut state, self);
        *self.map.get_mut(entity.entity_id).unwrap() = Some(Box::new(state));
        result
    }
}
</code></pre>
<p>The method receives a closure which is yielded a mutable reference to the entity it wants to update. The <code>expect</code> after <code>take</code> panics in case of reentrancy: if the entity is already being updated, we cannot call <code>update</code> on it again. This is why we chose to use <code>Option</code> as the value type in the <code>SlotMap</code> . It gives us a way to signal that this value is not available right now.</p>
<h3>Dropping stale entities</h3>
<p>When there are no more handles pointing to a state in the map, the state is marked as stale and should be removed.</p>
<p>To achieve this, we add a <code>pending</code> field to our <code>App</code> struct to keep track of the stale entities:</p>
<pre><code class="language-rust">pub struct App {
  map: SlotMap&lt;DefaultKey, Option&lt;Box&lt;dyn Any&gt;&gt;&gt;,
  pending: Rc&lt;RefCell&lt;Vec&lt;DefaultKey&gt;&gt;&gt;
}
</code></pre>
<p>When an entity handle is dropped, it checks if it is the only handle remaining to the state. If no other handle remains, it pushes its <code>entity_id</code> onto the <code>pending</code> queue.</p>
<p><code>Entity</code> should have access to the <code>pending</code> queue. We give it a <code>Weak</code> reference rather than an <code>Rc</code>, so that holding an <code>Entity</code> never keeps <code>App</code>'s queue (and thus <code>App</code> itself) artificially alive:</p>
<pre><code class="language-rust">pub struct Entity&lt;T&gt; {
  entity_id: DefaultKey,
  ref_count: Rc&lt;()&gt;,
  pending: Weak&lt;RefCell&lt;Vec&lt;DefaultKey&gt;&gt;&gt;,
  _marker: PhantomData&lt;T&gt;
}
</code></pre>
<p>With this new field in place, we need to update <code>new_entity</code> to populate it whenever an entity is created:</p>
<pre><code class="language-rust">pub fn new_entity&lt;T: 'static&gt;(&amp;mut self, build: impl FnOnce(&amp;App) -&gt; T) -&gt; Entity&lt;T&gt; {
        let entity = build(self);
        let entity_id = self.map.insert(Some(Box::new(entity)));
        Entity {
            entity_id,
            ref_count: Rc::new(()),
            pending: Rc::downgrade(&amp;self.pending),
            _marker: PhantomData,
        }
    }
</code></pre>
<p>Then we implement <code>Drop</code> for <code>Entity</code>, so that when the last handle to a state goes out of scope, its slot is queued for removal:</p>
<pre><code class="language-rust">impl&lt;T&gt; Drop for Entity&lt;T&gt; {
    fn drop(&amp;mut self) {
        if Rc::strong_count(&amp;self.ref_count) == 1
            &amp;&amp; let Some(queue) = self.pending.upgrade()
        {
            queue.borrow_mut().push(self.entity_id);
        }
    }
}
</code></pre>
<p>Finally, we implement a method called <code>flush_entities</code> which drains the queue and actually removes those stale slots from the map:</p>
<pre><code class="language-rust">impl App {
  pub fn flush_entities(&amp;mut self) {
        let ids: Vec&lt;DefaultKey&gt; = self.pending.borrow_mut().drain(..).collect();
        for id in ids {
            self.map.remove(id);
        }
  }
}
</code></pre>
<h2>Usage</h2>
<p>Let's test our framework by creating a simple Ratatui application. In the majority of applications, one component updates a state, and another one reads from that state.</p>
<p>In our test application, we will demonstrate this by creating two widgets. One will act as an input and update an <code>input_buffer</code>. The second widget will just read the <code>input_buffer</code> and reflect its changes (In real world applications, this second widget might use the input to filter search results for example).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6acbb58c99473e7a83b7ff75/c78c4792-2de9-43b6-82b1-30c2c31ffea8.png" alt="" style="display:block;margin:0 auto" />

<p>We will start by creating a <code>Root</code> struct. The <code>Root</code> struct holds a reference to an <code>App</code> struct:</p>
<pre><code class="language-rust">struct Root&lt;'a&gt; {
  app: &amp;'a mut App,
  widgets: Vec&lt;Box&lt;dyn Component&gt;&gt;
}
</code></pre>
<p>The <code>Root</code> also holds a list of widgets, defined using the <code>Component</code> trait. We define the <code>Component</code> trait as follows:</p>
<pre><code class="language-rust">trait Component {
    fn render_with_cx(&amp;mut self, cx: &amp;mut App, frame: &amp;mut Frame, area: Rect);
    fn handle_key(&amp;mut self, cx: &amp;mut App, key: KeyEvent);
}
</code></pre>
<p>Each component uses <code>render_with_cx</code> to draw on the frame while still having access to the context. Components also define a <code>handle_key</code> method in order to handle events.</p>
<p>Let us create our two widgets with their implementations of <code>Component</code>.</p>
<pre><code class="language-rust">struct InputWidget {
    input_buffer: Entity&lt;String&gt;,
}

struct EchoWidget {
    input_buffer: Entity&lt;String&gt;,
}
</code></pre>
<p>Each widget owns an <code>Entity&lt;String&gt;</code>. The input widget updates <code>input_buffer</code> and the echo widget reads it and displays it.</p>
<pre><code class="language-rust">impl InputWidget {
    fn init(input_buffer: Entity&lt;String&gt;) -&gt; Self {
        Self { input_buffer }
    }
}

impl EchoWidget {
    fn init(input_buffer: Entity&lt;String&gt;) -&gt; Self {
        Self { input_buffer }
    }
}
</code></pre>
<pre><code class="language-rust">impl Component for InputWidget {
    fn render_with_cx(&amp;mut self, cx: &amp;mut App, frame: &amp;mut Frame, area: Rect) {
        let block = Block::bordered()
            .title("Input")
            .padding(Padding::uniform(1));
        let inner = block.inner(area);
        block.render(area, frame.buffer_mut());

        let input_buffer = cx.read(&amp;self.input_buffer);
        let text_width = input_buffer.chars().count() as u16;

        frame.set_cursor_position(Position::new(inner.x + text_width, inner.y));
        Paragraph::new(&amp;input_buffer[..])
            .wrap(Wrap { trim: false })
            .render(inner, frame.buffer_mut());
    }

    fn handle_key(&amp;mut self, cx: &amp;mut App, key: KeyEvent) {
        match key.code {
            KeyCode::Char(c) =&gt; cx.update(self.input_buffer.clone(), |input_buffer, _cx| {
                input_buffer.push(c);
            }),
            KeyCode::Backspace =&gt; cx.update(self.input_buffer.clone(), |input_buffer, _cx| {
                if input_buffer.len() &gt; 0 {
                    input_buffer.remove(input_buffer.len() - 1);
                }
            }),
            _ =&gt; {}
        }
    }
}
</code></pre>
<p>In <code>render_with_cx</code> we render a block and a paragraph. To learn more about widgets and how to render them, you can check the ratatui documentation. The interesting part is that the paragraph is initialised with the <code>input_buffer</code> contents, thus the call to <code>cx.read(&amp;self.input_buffer)</code>.</p>
<p>In <code>handle_key</code> we listen to key events and then update our <code>input_buffer</code> state with <code>cx.update()</code>.</p>
<p>The <code>EchoWidget</code> component only reads the input buffer entity and displays it:</p>
<pre><code class="language-rust">impl Component for EchoWidget {
    fn render_with_cx(&amp;mut self, cx: &amp;mut App, frame: &amp;mut Frame, area: Rect) {
        let block = Block::bordered()
            .title("Echo")
            .padding(Padding::uniform(1));
        let inner = block.inner(area);
        block.render(area, frame.buffer_mut());

        let input_buffer = cx.read(&amp;self.input_buffer);
        Paragraph::new(&amp;input_buffer[..])
            .wrap(Wrap { trim: false })
            .render(inner, frame.buffer_mut());
    }

    // Echo widget is read-only: it never receives input focus or key events.
    fn handle_key(&amp;mut self, _cx: &amp;mut App, _key: KeyEvent) {}
}
</code></pre>
<p>Entity creation happens when initialising the <code>Root</code> struct by calling <code>cx.new_entity</code>:</p>
<pre><code class="language-rust">impl&lt;'a&gt; Root&lt;'a&gt; {
    fn init(cx: &amp;'a mut App) -&gt; Root&lt;'a&gt; {
        let input_buffer = cx.new_entity(|_| "Enter Text".into());
        Root {
            app: cx,
            widgets: vec![
                Box::new(InputWidget::init(input_buffer.clone())),
                Box::new(EchoWidget::init(input_buffer.clone())),
            ],
        }
    }
}
</code></pre>
<p>We also define a <code>run</code> method for <code>Root</code> which does the drawing by calling the <code>render_with_cx</code> method on each component, and listens for events by calling <code>handle_key_event</code> on each component in the root.</p>
<pre><code class="language-rust">impl&lt;'a&gt; Root&lt;'a&gt; {
    fn run(&amp;mut self, terminal: &amp;mut DefaultTerminal) -&gt; anyhow::Result&lt;()&gt; {
        loop {
            self.app.flush_entities();
            let _ = terminal.draw(|frame| {
                let [top, bottom] =
                    Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
                        .areas(frame.area());
                self.widgets[0].render_with_cx(self.app, frame, top);
                self.widgets[1].render_with_cx(self.app, frame, bottom);
            });

            if let Event::Key(key_event) = event::read()? {
                if !key_event.is_press() {
                    continue;
                }
                let is_quit = key_event.code == KeyCode::Esc
                    || (key_event.code == KeyCode::Char('c')
                        &amp;&amp; key_event.modifiers.contains(KeyModifiers::CONTROL));
                if is_quit {
                    break Ok(());
                }
                // Only the input widget (top, index 0) is interactive.
                self.widgets[0].handle_key(self.app, key_event);
            }
        }
    }
}
</code></pre>
<p>Finally, we define our main function:</p>
<pre><code class="language-rust">fn main() {
    let mut app: App = App::init();
    let mut root = Root::init(&amp;mut app);
    let _ = ratatui::run(|terminal| root.run(terminal));
}
</code></pre>
<h2>Conclusion</h2>
<p>Using our state management crate inspired by gpui, we were able to effectively manage state without dealing with the complexities that the borrow checker brings. Rather than wrapping each piece of shared state in an <code>Rc&lt;RefCell&lt;T&gt;&gt;</code>, as is common in Rust UI code, our state lives in a single <code>SlotMap</code> owned by the <code>App</code>. Components simply hold cheap <code>Entity&lt;T&gt;</code> handles into that map, instead of sharing mutable references directly.</p>
<p>The state management crate we developed here is experimental. It could be enhanced with other features, mainly:</p>
<ul>
<li><p>Only redraw when the UI is marked as dirty, instead of each frame.</p>
</li>
<li><p>Components subscribing to/observing a certain state. (We didn't have to deal with this here, since we redraw the entire UI each tick.)</p>
</li>
</ul>
<p>Code is available on GitHub:</p>
<ul>
<li><p><a href="https://gist.github.com/sonOfTheComet-ctrl/f34b16aa75c944b0bfbc990f5aea6795">State Management Crate</a></p>
</li>
<li><p><a href="https://gist.github.com/sonOfTheComet-ctrl/a9b08d8068e6377f6d2f62214199b345">Test Application</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>