Skip to content

Instantly share code, notes, and snippets.

@cecton
Last active March 29, 2021 13:30
Show Gist options
  • Save cecton/e06bbccbfaf7b369864e912e990955c1 to your computer and use it in GitHub Desktop.
Save cecton/e06bbccbfaf7b369864e912e990955c1 to your computer and use it in GitHub Desktop.
Proposal for a Yew more rust-y

Proposal for a Yew more rust-y

This document will describe structural changes that can be done to Yew to make it more Rust-y and less React-like. The goal is to improve the ergonomic for Rust users.

Table of content

Usage

Props (properties) as first class citizen

In Yew a component is a struct and the props are usually stored in a field of this struct:

pub struct Props {
    pub user_input_field: u32,    // public
}

pub struct MyComponent {
    props: Props,                 // private
}

impl yew::Component for MyComponent {
    type Properties = Props;

    // ...
}

A stateless component (purely visual) is made by not using its state but there are still 2 structs needed to make a component.

A new way to achieve this would be to implement a component on a "props" struct instead of a state struct:

// Our component & our props
pub struct MyComponent {
    pub user_input_field: u32,    // public
    state: MyState,               // private
}

// private state
struct State {
    internal_field: u32,          // private
}

impl yew::Component for MyComponent {
    // ...
}

Note how the type attribute disappeared.

This change will greatly simplify components that are purely visual and have no state (stateless component, aka pure components):

pub struct Title {
    pub class: Classes,
    pub children: html::Children,
}

impl Component for Title {
    fn view(&self) -> Html {
        html! {
            <h1 class=classes!("my-style-title", self.class.clone())>
                {self.children.clone()}
            </h1>
        }
    }
}

Implementing a state

The state can be implemented with more flexibility. The user define how and what they would use to implement a state.

Examples:

  • the user can use a local state stored in a private field in the props
  • the user can use a global variable with a store for their whole application

Local state

// Our component & our props
pub struct MyComponent {
    pub user_input_field: u32,    // public
    state: MyState,               // private
}

// private state
struct State {
    internal_field: u32,          // private
}

impl yew::Component for MyComponent {
    fn view(&self) -> Html {
        html! {
            <ul>
                <li>{ self.user_input_field }</li>          // retrieve a prop
                <li>{ self.state.internal_field }</li>      // retrieve state field
            </ul>
        }
    }
}

Here the state is local to the component just like class-based components in React or React's hooks.

A user could implement a state management library that would be similar to React's hooks if they want.

Global state

pub static STATE: MyState = MyState {
    internal_field: 42,
};

// Our component & our props
pub struct MyComponent {
    pub user_input_field: u32,    // public
}

// private state
struct State {
    internal_field: u32,          // private
}

impl yew::Component for MyComponent {
    fn view(&self) -> Html {
        html! {
            <ul>
                <li>{ self.user_input_field }</li>          // retrieve a prop
                <li>{ STATE.internal_field }</li>           // retrieve state field
            </ul>
        }
    }
}

Here the state come from a global variable. This could be used to implement a redux-store-like state.

Implementation

A component is updated when its props change. This mean a component must implement PartialEq in order to check if any field changed.

More than that, a component needs that every of its field be updated only if a change is detected. In order to do that, we would need to introduce a new trait that can be called Property. This would be its implementation:

pub trait Property: Sized {
    fn update(&mut self, new_value: Self) -> bool;
}

// `Property` would be implemented automatically for all built-in (and not built-in) types
// that implement `PartialEq`
impl<T: PartialEq> Property for T {
    fn update(&mut self, new_value: Self) -> bool {
        // change detection
        if self == new_value {
            // new_value is dropped because no change has been detected
            false
        } else {
            // re-assign the value with the new value and request an update
            *self = new_value;
            true
        }
    }
}

With the trait Property, every field can be updated individually:

let mut class = String::from("class1");

let did_update = class.update(String::from("class1"));
println!("class={:?} did_update={}", class, did_update);    // class="class1" did_update=false

let did_update = class.update(String::from("class2"));
println!("class={:?} did_update={}", class, did_update);    // class="class2" did_update=true

The Properties derive macro

There will also be a derive macro that update all the fields and return true if any field updated:

#[derive(Properties)]
pub struct MyComponent {
    pub class: String,
    pub children: Html,
    // private, opaque type for the sake of the example, it implements `Default` and `Property`
    state: OpaqueComponentState,
}

// `Properties` expand to `Property` in order to get the update() function on the whole component
impl Property for MyComponent {
    fn update(&mut self, new_value: Self) -> bool {
        let mut res = false;
        res |= self.class.update(new_value.class);
        res |= self.children.update(new_value.children);
        res |= self.state.update(new_value.state);
        res
    }
}

// `Properties` should also expand to a builder that allows the user to instantiate the component
// without providing the private fields (the state)
//
// This is an example and might not be the best solution.
//
pub struct MyComponentBuilder<A, B> {
    class: A,
    children: B,
}

impl MyComponent {
    pub fn builder() -> MyComponentBuilder<(), ()> {
        MyComponentBuilder {
            class: (),
            children: (),
        }
    }
}

impl MyComponentBuilder<A, B> {
    fn with_class<T>(self, class: T) -> MyComponentBuilder<T, B> {
        MyComponentBuilder {
            class,
            ..self
        }
    }

    fn with_children<T>(self, children: T) -> MyComponentBuilder<A, T> {
        MyComponentBuilder {
            children,
            ..self
        }
    }
}

impl MyComponentBuilder<String, Html> {
    fn build(self) -> MyComponent {
        MyComponent {
            class: self.class,
            children: self.children,
            // any private field will use the Default impl
            state: Default::default(),
        }
    }
}

Rc

Rc is a central type used in Yew. The lack of garbage collector makes any Yew application heavily rely on Clone. To avoid cloning big chunk of memory, the user should user Rc.

Rc implementes PartialEq only if the inner type implements PartialEq. This is okay in most cases but the user will want to provide closures and other types that do no implement PartialEq. To solve this issue, Yew should provide another Rc that will implement PartialEq using Rc::ptr_eq.

Example:

// public API for Yew users
pub struct RcWrapper<T> {
    inner: std::rc::Rc<T>,
}

impl<T> PartialEq for RcWrapper<T> {
    fn eq(&self, other: &Self) -> bool {
        // the `Property` will be automatically implemented because this `Rc` implements
        // `PartialEq`. The fields that are using this new `RcWrapper` will be updated if the
        // reference changes
        std::rc::Rc::ptr_eq(&self.inner, &other.inner)
    }
}

Usage:

pub struct MyButton {
    pub on_click: RcWrapper<Fn(MouseEvent)>,
}

impl Component for MyButton {
    fn view(&self) -> Html {
        html! {
            <button onclick={ self.on_click.clone() } />
        }
    }
}

Note how a closure of any type and any signature can be passed as argument. This is the equivalent of the existing Callback struct but more flexible.

Local state management

To avoid unnecessary state initialization. Every local state should be wrapped in a type that delay the initialization of the state. It will also handle the need for update of a component: if the state changed, the component's "state" property will ask for an update of the component.

Example:

// provided by Yew's API
pub struct State<T: Default> {
    inner: Option<Box<std::cell::RefCell<T>>>,
    need_update: std::cell::RefCell<bool>,
}

impl<T: Default> Default for State<T> {
    fn default() -> Self {
        Self {
            // the default implementation will return an uninitialized state
            inner: None,
            need_update: std::cell::RefCell::new(false),
        }
    }
}

impl<T: Default> Property for State<T> {
    fn update(&mut self, _new_value: Self) -> bool {
        let mut need_update = self.need_update.borrow_mut();
        if *need_update {
            *need_update = false;
            true
        } else {
            false
        }
    }
}

impl<T: Default> State<T> {
    // immutably borrow the state, don't need to update the component
    pub fn borrow(&self) -> std::cell::Ref<T> {
        self.inner.as_ref().expect("the state has been initialized before").borrow()
    }

    // mutably borrow the state, the component will need an update
    pub fn borrow_mut(&self) -> std::cell::RefMut<T> {
        *self.need_update.borrow_mut() = true;
        self.inner.as_ref().expect("the state has been initialized before").borrow_mut()
    }
}

Usage:

// public
pub struct TodoItem {
    // public (through props)
    pub name: String,
    // private
    state: State<TodoState>,
}

struct TodoState {
    checked: bool,
}

impl yew::Component for TodoItem {
    fn view(&self) -> Html {
        let state = self.state.borrow();

        html! {
            <div class=classes!("todo-item-row")>
                <div class=classes!("todo-item-name")>{ self.name.clone() }</div>
                <div
                    // add the class "checked" if the state's checked is `true`
                    class=classes!("todo-item-checkmark", state.checked.then("checked"))
                    onclick=// handle click event using the state somehow (with messages for example)
                />
            </div>
        }
    }
}

Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment