diff --git a/src/buffers/mod.rs b/src/buffers/mod.rs
index 89c5d35..9d454d4 100644
--- a/src/buffers/mod.rs
+++ b/src/buffers/mod.rs
@@ -20,14 +20,14 @@ use futures::StreamExt;
 use matrix_sdk::async_trait;
 use nonempty::NonEmpty;
 use ratatui::text::Text;
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 
 mod log;
 pub use log::LogBuffer;
 mod room;
 pub use room::RoomBuffer;
 
-#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum BufferId {
   /// The main/home buffer
   Log,
@@ -57,6 +57,9 @@ pub trait Buffer: Send + Sync + memuse::DynamicUsage {
   fn parent(&self) -> Option<BufferId> {
     None
   }
+  fn children(&self) -> Option<Vec<BufferId>> {
+    None
+  }
   /// Returns if there are any updates to apply.
   async fn poll_updates(&mut self);
   fn content<'a>(&'a self) -> Box<dyn Iterator<Item = BufferItem<'a>> + 'a>;
@@ -75,6 +78,12 @@ pub trait Buffer: Send + Sync + memuse::DynamicUsage {
 
 pub struct Buffers {
   buffers: Vec<Box<dyn Buffer>>,
+  parents: HashMap<BufferId, HashSet<BufferId>>,
+  children: HashMap<BufferId, HashSet<BufferId>>,
+  /// Set of buffers already placed after a parent space, so other spaces should not
+  /// steal them, even if they are also their child (or it would cause buffers to move
+  /// every time we re-sort the buffer list)
+  attached_to_parent: HashSet<BufferId>,
   active_index: usize,
 }
 
@@ -82,6 +91,9 @@ impl Buffers {
   pub fn new(initial_buffer: Box<dyn Buffer>) -> Self {
     Self {
       buffers: vec![initial_buffer],
+      children: HashMap::new(),
+      parents: HashMap::new(),
+      attached_to_parent: HashSet::new(),
       active_index: 0,
     }
   }
@@ -98,6 +110,9 @@ impl Buffers {
     // Reorder buffers in case we just got an update on space relationships
     // FIXME: do this only when needed
     let mut buffers = Vec::new();
+    //self.children.clear();
+    //self.parents.clear();
+    self.attached_to_parent.clear();
     std::mem::swap(&mut self.buffers, &mut buffers);
     for buf in buffers.into_iter() {
       self.push(buf);
@@ -108,6 +123,14 @@ impl Buffers {
     self.buffers.len()
   }
 
+  pub fn parents(&self) -> &HashMap<BufferId, HashSet<BufferId>> {
+    &self.parents
+  }
+
+  pub fn children(&self) -> &HashMap<BufferId, HashSet<BufferId>> {
+    &self.children
+  }
+
   pub fn iter(&self) -> impl Iterator<Item = &dyn Buffer> {
     self.buffers.iter().map(|buffer_box| &**buffer_box)
   }
@@ -120,31 +143,72 @@ impl Buffers {
   pub fn push(&mut self, new_buffer: Box<dyn Buffer>) {
     let new_buffer_id = new_buffer.id();
 
-    match new_buffer
-      .parent()
-      .and_then(|parent_id| self.buffers.iter().position(|buf| buf.id() == parent_id))
-    {
+    match new_buffer.children() {
+      Some(children) => {
+        for child in children.iter() {
+          self
+            .parents
+            .entry(child.clone())
+            .or_insert_with(HashSet::new)
+            .insert(new_buffer_id.clone());
+        }
+        self
+          .children
+          .entry(new_buffer_id.clone())
+          .or_insert_with(HashSet::new)
+          .extend(children.into_iter());
+      },
+      None => {},
+    };
+    let parent = match new_buffer.parent() {
+      Some(parent) => {
+        self
+          .parents
+          .entry(new_buffer_id.clone())
+          .or_insert_with(HashSet::new)
+          .insert(parent.clone());
+        self
+          .children
+          .entry(parent.clone())
+          .or_insert_with(HashSet::new)
+          .insert(new_buffer_id.clone());
+        Some(parent)
+      },
+      None => self
+        .parents
+        .get(&new_buffer_id)
+        .and_then(|parents| parents.iter().min().cloned()), // .min() for determinism
+    };
+    let children = self
+      .children
+      .entry(new_buffer_id.clone())
+      .or_insert_with(HashSet::new);
+
+    match parent.and_then(|parent_id| self.buffers.iter().position(|buf| buf.id() == parent_id)) {
       None => self.buffers.push(new_buffer),
       Some(parent_position) => {
-        // iterator through buffers after the parent, and record on the stack the chain
+        // iterate through buffers after the parent, and record on the stack the chain
         // of successor from the parent to the current buffer. As soon as we see a buffer
         // with a parent not on the stack (or with no parent), it means we left the subtree,
         // and should insert the new buffer there.
         let mut stack = vec![self.buffers[parent_position].id()];
         let mut new_buffer = Some(new_buffer);
         for (i, buf) in self.buffers.iter().enumerate().skip(parent_position + 1) {
-          match buf.parent() {
-            None => {}, // root buffer
-            Some(parent_id) => {
-              match stack.iter().position(|id| *id == parent_id) {
-                None => {}, // parent is not in the subtree
-                Some(parent_position) => {
-                  // parent is in the subtree
-                  stack.truncate(parent_position + 1);
-                  stack.push(buf.id());
-                  continue;
-                },
-              }
+          match self
+            .parents
+            .entry(buf.id())
+            .or_insert_with(HashSet::new)
+            .iter()
+            .flat_map(|parent_id| stack.iter().position(|id| *id == *parent_id))
+            .min() // for determinism
+          {
+            None => {}, // root buffer, or parent is not in the subtree
+            Some(parent_position) => {
+              // parent is in the subtree
+              stack.truncate(parent_position + 1);
+              stack.push(buf.id());
+              self.attached_to_parent.insert(new_buffer_id.clone());
+              continue;
             },
           }
           // if we are here, it means buf.parent() is not in the subtree (or buf is orphan)
@@ -165,39 +229,21 @@ impl Buffers {
     // Find existing children and move them after this buffer (while preserving their
     // relative order.
 
-    // 1. collect the set of children
-    let mut children_ids = HashSet::new();
-    children_ids.insert(new_buffer_id.clone());
-    let mut added_children_ids = true;
-    while added_children_ids {
-      added_children_ids = false;
-      for buf in self.buffers.iter() {
-        if children_ids.contains(&buf.id()) {
-          // Already collected
-          continue;
-        }
-        if let Some(parent) = buf.parent() {
-          if children_ids.contains(&parent) {
-            children_ids.insert(buf.id());
-          }
-        }
-      }
-    }
-    children_ids.remove(&new_buffer_id);
-
-    // 2. pop them from the list of buffers
+    // 1. pop children from the list of buffers
+    // FIXME: should be recursive
     let mut all_buffers = Vec::new();
     let mut child_buffers = Vec::new();
     std::mem::swap(&mut all_buffers, &mut self.buffers);
     for buf in all_buffers.into_iter() {
-      if children_ids.contains(&buf.id()) {
-        child_buffers.push(buf)
+      if children.contains(&buf.id()) && !self.attached_to_parent.contains(&buf.id()) {
+        self.attached_to_parent.insert(buf.id());
+        child_buffers.push(buf);
       } else {
         self.buffers.push(buf);
       }
     }
 
-    // 3. split the set of buffers, and insert the children after the new buffer
+    // 2. split the set of buffers, and insert the children after the new buffer
     let after_children = self.buffers.split_off(
       self
         .buffers
diff --git a/src/buffers/room.rs b/src/buffers/room.rs
index ff379e7..64bb82a 100644
--- a/src/buffers/room.rs
+++ b/src/buffers/room.rs
@@ -14,6 +14,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use std::collections::HashSet;
 use std::sync::atomic::{AtomicU16, Ordering};
 use std::sync::{Arc, OnceLock};
 
@@ -25,7 +26,10 @@ use futures::stream::FuturesUnordered;
 use futures::{FutureExt, Stream, StreamExt};
 use itertools::Itertools;
 use matrix_sdk::async_trait;
+use matrix_sdk::deserialized_responses::SyncOrStrippedState;
 use matrix_sdk::room::ParentSpace;
+use matrix_sdk::ruma::events::space::child::SpaceChildEventContent;
+use matrix_sdk::ruma::events::SyncStateEvent;
 use matrix_sdk::ruma::{OwnedRoomId, RoomId};
 use matrix_sdk::{Client, DisplayName, Room, RoomInfo};
 use matrix_sdk_ui::timeline::{
@@ -331,6 +335,7 @@ pub struct RoomBuffer {
 
   initialized_roominfo: bool,
   parent: Option<OwnedRoomId>,
+  children: Option<Vec<OwnedRoomId>>,
   display_name: Option<DisplayName>,
 
   // It's unlikely users will join the same room with more than one account;
@@ -362,6 +367,7 @@ impl RoomBuffer {
       room_id,
       initialized_roominfo: false,
       parent: None,
+      children: None,
       display_name: None,
       buffers: SmallVec::new(),
     };
@@ -402,8 +408,9 @@ impl RoomBuffer {
   }
 
   async fn update_room_info(&mut self, room: &Room) {
-    (self.parent, self.display_name) = tokio::join!(
+    (self.parent, self.children, self.display_name) = tokio::join!(
       async {
+        // Get parent space
         match room.parent_spaces().await {
           Ok(parents) => {
             parents
@@ -424,6 +431,37 @@ impl RoomBuffer {
           },
         }
       },
+      async {
+        // Get child rooms
+        match room
+          .get_state_events_static::<SpaceChildEventContent>()
+          .await
+        {
+          Ok(child_events) => {
+            Some(
+              child_events
+                .into_iter()
+                // Extract state key (ie. the child's id) and sender
+                .flat_map(|parent_event| {
+                  match parent_event.deserialize() {
+                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Original(e))) => {
+                      Some(e.state_key.to_owned())
+                    },
+                    Ok(SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_))) => None,
+                    Ok(SyncOrStrippedState::Stripped(e)) => Some(e.state_key.to_owned()),
+
+                    Err(_) => None, // Ignore deserialization errors
+                  }
+                })
+                .collect(),
+            )
+          },
+          Err(e) => {
+            tracing::error!("Failed to get child rooms of {}: {:?}", self.room_id, e);
+            None
+          },
+        }
+      },
       async {
         match room.display_name().await {
           Ok(dn) => {
@@ -480,6 +518,14 @@ impl Buffer for RoomBuffer {
       .as_ref()
       .map(|parent| BufferId::Room(parent.to_owned()))
   }
+  fn children(&self) -> Option<Vec<BufferId>> {
+    self.children.as_ref().map(|children| {
+      children
+        .iter()
+        .map(|child| BufferId::Room(child.to_owned()))
+        .collect()
+    })
+  }
 
   async fn poll_updates(&mut self) {
     let room = if self.initialized_roominfo {
diff --git a/src/components/buflist.rs b/src/components/buflist.rs
index 2c5136b..8dfa3f7 100644
--- a/src/components/buflist.rs
+++ b/src/components/buflist.rs
@@ -72,10 +72,15 @@ impl Component for Buflist {
           .iter()
           .enumerate()
           .map(|(i, buf)| {
-            match buf.parent() {
-              Some(parent) => {
-                stack.truncate(stack.iter().position(|id| *id == parent).unwrap_or(0) + 1)
-              },
+            match buffers
+              .parents()
+              .get(&buf.id())
+              .into_iter()
+              .flatten()
+              .flat_map(|parent| stack.iter().position(|id| *id == *parent))
+              .max() // if in multiple spaces, the last one is most likely to matter
+            {
+              Some(parent_position) => stack.truncate(parent_position + 1),
               None => stack.clear(),
             }
             stack.push(buf.id());