UI development: React-like state diff?


#1

I’m trying to build a user interface in Rust.

I’d like to describe the state of the app as a struct and, like React, have a render() function that would rerender the UI when the state changes. But I’d like to only render what has changed.

So I would have state1 and state2, I’d like to get a diff. So far, I’ve been using https://github.com/Byron/treediff-rs that basically does a JSON diff between the structures and gives me back a list of changed properties as an array of strings.

In the render function, I would go through the diff, and compare the string with the field of my struct.

This is getting very cumbersome as everytime I add a field to my struct, I have to make sure to update my render functions. And if I make a mistake or forget something, it fails during the runtime, not at compile time, because I compare strings.

Would there be a smarter way of doing this?

Ideally, I would have an arbitrary struct, and a macro or something would generate a list of possible diff, as an enum, so a missing check would be noticed at compile time, not at runtime.

Also, maybe there is a better alternative to treediff, something that doesn’t require serialization.

Thank you.


#2

Can you point us to an example implementation of your render() function? Depending on how it’s implemented, you may be able to take some inspiration from games, seeing as game rendering can sometimes be quite similar to UI rendering.

I believe conrod (a UI library built on top of piston) uses this react-style diff method.

Conrod aims to adopt the best of both worlds by providing an immediate mode API over a hidden, retained widget state graph. From the user’s perspective conrod widgets tend to feel stateless, as though they only exist for the lifetime of the scope in which they are instantiated. In actuality, each widget’s state is cached (or “retained”) within the Ui’s internal widget graph. This allows for efficient re-use of allocations and the ability to easily defer the drawing of widgets to a stage that is more suited to the user’s application.


#3

I use Cocoa for the UI.

The state would be something like:

pub struct WindowState {
    pub sidebar_is_open: bool,
    pub logs_visible: bool,
    pub status: Option<String>,
    pub options_open: bool,
    pub title: String,
}

I would manually map all the possible fields name to an enum:

pub enum DiffKey {
    sidebar_is_open,
    logs_visible,
    status,
    options_open,
    title,
}

I use treediff to get a diff and map the strings to DiffKey.

And then do the rendering:

    fn render(&self, diff: Vec<ChangeType>, state: &WindowState) {

        for change in diff {
            use self::DiffKey as K;
            match change {
                ChangeType::Modified(keys) => {
                    match keys.as_slice() {
                        &[K::tabs, K::Index(i), K::Alive, ref attr] if idx == i => {
                            match *attr {
                                K::sidebar_is_open => {
                                    toggle_sidebar(state.sidebar_is_open)
                                }
                               …

This is very fragile as:

  1. i might be forgetting a key in the diff-string to DiffKey
  2. i might be forgetting a change in the render loop (the match can’t check all the possible changes have been covered)

#4

Yeah, just looking at it that seems to be a maintenance nightmare waiting to happen…

How does React deal with this? I know the idea is you render to a shadow DOM and then if a particular node changes at all you re-render all of its components. You could use try to iterate over both DOM trees in parallel (kinda like zip()) and then if the nodes are different (PartialEq) you’ll re-render the entire sub-tree.

Unless you’re encountering noticeable performance issues, it may be easier to use the “naive” method of checking for equality and subtree re-rendering, instead of a more complicated algorithm based around diffs and changesets.


#5

Iterating through the serialized version of the 2 structs will end up looking a lot like my current setup where I will have an array of strings pointing to the property that has changed.

Ideally, I would have a macro that would create an enum of all the possible changes out my struct.

Not sure if that makes sense…


#6

I think you could do a lot better than that in Rust. Having an array of strings pointing to the property that changed means you’ll now have a “stringly-typed” API, where you can only determine errors at runtime.

A different way of doing things would be for the render() method to actually return a DOM node for our shadow DOM. The idea being the DOM nodes returned are meant to be fairly cheap to copy around, and allow any arbitrary UI component to be composed of a set of DOM nodes.

pub trait Render {
  fn render(&self) -> Node;
}

pub struct Node {
  properties: Vec<Property>,
  kind: NodeType,
}

pub enum NodeType {
  Container(Vec<Node>),
  Text(String),
  ...
}

You can diff the nodes easily enough, and because the DOM tree is strongly typed (we’re using structs and enums, instead of strings), a lot of issues with the earlier stringly-typed API won’t be possible by design (e.g. typo in properties, forgetting to update render()).


#7

You want to write a derive plugin, I think.


#8

Yes. Having an intermediate DOM will absolutely help. Diffing the DOM is a lot easier than diffing the state.

Thank you!


#9

Reading about Derive. I might end up using that. Thank you :slight_smile:


#10

I’ve also been playing around with the idea of a native UI(based on gtk-rs) library in rust with a react-like api(using rsx).State diffing and component recomputing is imo inefficient. Mobx - a state management library for react uses observable data structures and observers.

how it works is simple, instead of re-computing all children nodes & then diffing the dom output whenever state changes, You would instead have observable data structures, in which only components subscribed to said data are re-rendered when it changes. This is achieved using getters and setters. Much more efficient imo and definately the optimal approach. Still haven’t figured out how to implement “observable structs” + “getters & setters” in rust. so I’ve been pretty much stuck.