-
Hi there, I understand that dominator uses reactivity for fine grained updates. I'm just wondering how to handle receiving fresh state from the server. In the ELM/React model it's pretty simple conceptually as the entire view is a pure function of state, and I guess that still holds here, but it "feels" like dominator is optimised for smaller updates? For example, if I'm display a list of Widgets I would have a For more context, the updates we get from the server are very coarse - literally, the entire state, regardless of what has changed, so if a new Widget is added then we receive the entire state over, and over (and over). Will it be expensive replacing the state and redrawing the entire UI from scratch every time? I'm asking the question because with a virtualdom this isn't a conceptual or practical issue as the performance happens in the virtualdom reconciliation. However, without that virtualdom, I can't see how the entire UI isn't redrawn over and over because the signals are replaced? Thanks! P.S I've posted the equivalent on sycamore-rs/sycamore#485 |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
For lists you generally want to use The reason for this is because
Ideally the server would send more fine-grained updates, it's most efficient to fix the problem at the source instead of working around it in the client. But I assume you can't change the server, in which case you have two options: 1: You can replace the entire state, which will redraw all of the DOM nodes. This is fine for small bits of DOM, but I don't think it will scale well for large amounts of DOM. 2: You can do value diffing. The way that it works is simple: you compare the old data to the new data from the server, and only update if the new data is different. Here is a simple example: let mut lock = self.my_state.lock_mut();
if *lock != new_server_state {
// Update the Mutable with the new server state
*lock = new_server_state;
} This pattern is so common that there is a shortcut for it called self.my_state.set_neq(new_server_state); This will only update Of course since you are supposed to be using many fine-grained Mutables, you'll need to handle each Mutable individually: // This is the data from the server
struct ServerFoo {
foo: u32,
bar: String,
qux: bool,
}
struct ServerBar {
foo: ServerFoo,
corge: f64,
}
// These are your dominator components that exist on the client
struct ClientFoo {
foo: Mutable<u32>,
bar: Mutable<String>,
qux: Mutable<bool>,
}
impl ClientFoo {
fn new(server: ServerFoo) -> Arc<Self> {
Arc::new(Self {
foo: Mutable::new(server.foo),
bar: Mutable::new(server.bar),
qux: Mutable::new(server.qux),
})
}
fn update_server(&self, server: ServerFoo) {
self.foo.set_neq(server.foo);
self.bar.set_neq(server.bar);
self.qux.set_neq(server.qux);
}
}
struct ClientBar {
foo: Arc<ClientFoo>,
corge: Mutable<f64>,
}
impl ClientBar {
fn new(server: ServerBar) -> Arc<Self> {
Arc::new(Self {
foo: ClientFoo::new(server.foo),
corge: Mutable::new(server.corge),
})
}
fn update_server(&self, server: ServerBar) {
// This calls ClientFoo::update_server
self.foo.update_server(server.foo);
self.corge.set_neq(server.corge);
}
} The above code may seem large, but it's just standard dominator components. The only difference is that we added a new When you receive new server data, you just call the For lists things get trickier. We don't currently have an API for reconciling two lists, and there are many different ways to do reconciliation. For example, if the lists are sorted, then you can use binary search. Or if you know that the list is append-only then you can use a more efficient algorithm, etc. Ideally the server gives you some sort of unique ID for each list element, this makes the algorithms simper and more efficient. But if it doesn't, then you'll have to handle list element identity somehow. This is similar to how VDOM frameworks have "keyed" and "non-keyed" lists. Because each situation is unique, you'll have to handle the value diffing yourself. But it's not that hard. Here is an example for updating a sorted list from the server: // This is the data you receive from the server
struct ServerFoo {
id: String,
foo: u32,
bar: String,
qux: bool,
}
struct ServerList {
sorted_foo: Vec<ServerFoo>,
}
// These are the dominator components that exist on the client
struct ClientFoo {
id: String,
foo: Mutable<u32>,
bar: Mutable<String>,
qux: Mutable<bool>,
}
impl ClientFoo {
fn new(server: ServerFoo) -> Arc<Self> {
Arc::new(Self {
id: server.id,
foo: Mutable::new(server.foo),
bar: Mutable::new(server.bar),
qux: Mutable::new(server.qux),
})
}
// This method defines the identity of the data.
//
// In this case we have a unique ID, so we can just use that.
//
// But if there isn't a unique ID then you will need to figure
// out the identity of the data (e.g. by using `cmp` on all of
// the struct's fields).
fn cmp(&self, server: &ServerFoo) -> Ordering {
self.id.cmp(&server.id)
}
fn update_server(&self, server: ServerFoo) {
self.foo.set_neq(server.foo);
self.bar.set_neq(server.bar);
self.qux.set_neq(server.qux);
}
}
struct ClientList {
sorted_foo: MutableVec<Arc<ClientFoo>>,
}
impl ClientList {
fn new(server: ServerList) -> Arc<Self> {
Arc::new(Self {
sorted_foo: MutableVec::new_with_values(
server.sorted_foo.into_iter()
.map(ClientFoo::new)
.collect()
),
})
}
// This does the value diffing for the sorted Vec
fn update_server(&self, server: ServerList) {
let mut lock = self.sorted_foo.lock_mut();
// Removes stale data which exists locally but doesn't exist on the server
lock.retain(|data| {
server.sorted_foo.binary_search_by(|x| data.cmp(&x).reverse()).is_ok()
});
for data in server.sorted_foo {
match lock.binary_search_by(|x| x.cmp(&data)) {
// Update existing data with server data
Ok(index) => {
// This calls the ClientFoo method that is defined above
lock[index].update_server(data);
},
// Add new data from server
Err(index) => {
lock.insert_cloned(index, ClientFoo::new(data));
},
}
}
}
} The code may look intimidating, but most of it is standard dominator component code. The only new part is the The If you have many sorted lists that you need to synchronize, you can put the above code into a helper function: trait Update {
type Server;
fn new(server: Self::Server) -> Self;
fn cmp(&self, server: &Self::Server) -> Ordering;
fn update(&self, server: Self::Server);
}
fn reconcile_sorted_lists<A>(client: &MutableVec<A>, server: Vec<A::Server>) where A: Update + Clone {
let mut lock = client.lock_mut();
// Removes data which exists locally but doesn't exist on the server
lock.retain(|data| {
server.binary_search_by(|x| data.cmp(&x).reverse()).is_ok()
});
for data in server {
match lock.binary_search_by(|x| x.cmp(&data)) {
// Update existing data with server data
Ok(index) => {
lock[index].update(data);
},
// Add new data from server
Err(index) => {
lock.insert_cloned(index, A::new(data));
},
}
}
} Then you just impl the impl Update for Arc<ClientFoo> {
type Server = ServerFoo;
fn new(server: Self::Server) -> Self {
Arc::new(ClientFoo {
id: server.id,
foo: Mutable::new(server.foo),
bar: Mutable::new(server.bar),
qux: Mutable::new(server.qux),
})
}
fn cmp(&self, server: &Self::Server) -> Ordering {
self.id.cmp(&server.id)
}
fn update(&self, server: Self::Server) {
self.foo.set_neq(server.foo);
self.bar.set_neq(server.bar);
self.qux.set_neq(server.qux);
}
} For non-sorted lists, you can implement the value diffing however you wish, just implement it however you normally would on fn update_server(&self, server: ServerList) {
let mut lock = self.unsorted_foo.lock_mut();
// Removes stale data which exists locally but doesn't exist on the server
lock.retain(|data| {
server.unsorted_foo.iter().position(|x| data.eq(x)).is_some()
});
for data in server.unsorted_foo {
match lock.iter().position(|x| data.eq(x)) {
// Update existing data with server data
Some(index) => {
// This calls the ClientFoo method that is defined above
lock[index].update_server(data);
},
// Add new data from server
None => {
lock.push_cloned(ClientFoo::new(data));
},
}
}
} Because This means each individual change is And so the DOM performance is
Performance-wise, VDOM frameworks often need to do VDOM reconciliation and value diffing (e.g. with |
Beta Was this translation helpful? Give feedback.
For lists you generally want to use
MutableVec
instead ofMutable
, becauseMutableVec
is much faster (updates areO(1)
instead ofO(n)
).The reason for this is because
MutableVec
processes the differences between changes, not the entire list. So because it's only processing the differences, it's much much much faster.Ideally the server would send more fine-grained updates, it's most efficient to fix the problem at the source instead of working around it in the client.
B…