chinedufn / percy

Build frontend browser apps with Rust + WebAssembly. Supports server side rendering.
https://chinedufn.github.io/percy/
Apache License 2.0
2.28k stars 84 forks source link

Use request_animation_frame from rust instead of Global Js call for update view #144

Open aldebaranzbradaradjan opened 4 years ago

aldebaranzbradaradjan commented 4 years ago

I'm playing with Percy, and it's pretty cool, really a good feeling. But i'm not satisfied by the way I update my views. In the isomorphic example you use a call to a function in JS from the rust listener to call a render code in the same crate, and you suggest to replace all of that by the use of request_animation_frame from rust : https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.request_animation_frame .

But i can't figure how to do that, can you help me ? By the way i use the method of global js call, it's working but i'm curious :)

I can't find a way to update my view when my data is modified. Redux provide listeners, so i can do that :

let listener: Subscription<State> = | state: &State | {
    web_sys::console::log_1( &format!("Counter changed! New value: {}", state.counter).into() ) ;
}

But to upgrade my view I need a ref to the updater in the closure, so, i have add a move before my closure like this this :

let listener: Subscription<State> = move | state: &State | {
    web_sys::console::log1( &format!("Counter changed! New value: {}", state.counter).into() ) ;
    app.render();
}

After that, subscribers complain that it is not a function but a closure :

|           let listener: Subscription<State> = move | state: &State | {
   |  __-------------------_^
   | |                       |
   | |                       expected due to this
73 | |             web_sys::console::log1( &format!("Counter changed! New value: {}", state.counter).into() ) ;
74 | |             app.render();
75 | |         };
   | |__^ expected fn pointer, found closure

I did a research, a closure can be converted to an anonymous function but not if it captures its environment. So when I use move, it's not convertible to function and it doesn't work anymore

Do you know how i can update my view with this callback ? I'm very new to Rust.

I put my code in example of the pb :

#![feature(proc_macro_hygiene)]

use wasm_bindgen::prelude::*;
use web_sys;
use css_rs_macro::css;
use virtual_dom_rs::prelude::*;

use std::cell::RefCell;
use std::rc::Rc;

use redux_rs::{Store};

//-------------------------------------------------------------------------------------------------------------------

#[wasm_bindgen]
extern "C" {
    pub type GlobalJS;
    pub static global_js: GlobalJS;

    #[wasm_bindgen(method)]
    pub fn update(this: &GlobalJS);
}
//-------------------------------------------------------------------------------------------------------------------

#[derive(Default)]
struct State {
    counter: u8
}

enum Action {
    Increment,
    Decrement
}

fn counter_reducer(state: &State, action: &Action) -> State {
    match action {
        Action::Increment => State {
            counter: state.counter + 1
        },
        Action::Decrement => State {
            counter: state.counter - 1
        }
    }
}

//-------------------------------------------------------------------------------------------------------------------

#[wasm_bindgen]
pub struct App {
    dom_updater: DomUpdater,
    store: Rc<RefCell<Store<State, Action>>>,
    home_view: HomeView,
}

#[wasm_bindgen]
impl App {

    #[wasm_bindgen(constructor)]
    pub fn new () -> App {

        let window = web_sys::window().unwrap();
        let document = window.document().unwrap();
        let body = document.body().unwrap();

        let store = Rc::new(RefCell::new(Store::new(counter_reducer, State::default())) );

        store.borrow_mut().subscribe( |state: &State| {
            global_js.update();
        });

        let view = HomeView {
            store : Rc::clone(&store)
        };

        App {
            dom_updater : DomUpdater::new_append_to_mount(view.render(), &body),
            store : Rc::clone(&store),
            home_view : view
        }
    }

    pub fn render( &mut self ) {
        self.dom_updater.update( self.home_view.render() );
    }
}

//-------------------------------------------------------------------------------------------------------------------

struct HomeView {
    store: Rc<RefCell<Store<State, Action>>>,
}

impl View for HomeView {
    fn render(&self) -> VirtualNode {
        let store = Rc::clone(&self.store);

        html! {
        <div>

        <ChildComponent store = { Rc::clone(&self.store) } >
        </ChildComponent>

        <button class=MY_COMPONENT_CSS onclick = move | _: web_sys::Event | {
            store.borrow_mut().dispatch(Action::Increment);
        }>
            Click me!
        </button>

        <textarea 
            oninput= |e: String | {
                web_sys::console::log_1(&format!("input : {:?}", e ).into());
            }

            placeholder="Type in this box. When you click away an alert will be generated.">
        </textarea>

        </div>
        }
    }
}

//-------------------------------------------------------------------------------------------------------------------

struct ChildComponent {
    store: Rc<RefCell<Store<State, Action>>>,
}

impl View for ChildComponent {
    fn render(&self) -> VirtualNode {
        html! {
            <div class = "big red" >
                Count is {format!("{}", self.store.borrow_mut().state().counter )}
            </div>
        }
    }
}

static MY_COMPONENT_CSS: &'static str = css!{r#"
    :host {
        font-size: 24px;
        font-weight: bold;
    }
"#};

static _MORE_CSS: &'static str = css!{r#"
    .big { font-size: 30px; }
    .red { color: red; }
"#};

It's working but use code from Js to call render in Rust, I would love to do thing like that :

    #[wasm_bindgen(constructor)]
    pub fn new () -> App {

        let window = web_sys::window().unwrap();
        let document = window.document().unwrap();
        let body = document.body().unwrap();

        let mut store = Rc::new(RefCell::new(Store::new(counter_reducer, State::default())));
        let view = HomeView::new( Rc::clone(&store) );

        let app = App {
            dom_updater : DomUpdater::new_append_to_mount(view.render(), &body),
            store : Rc::clone(&store),
            home_view : view
        };

        let listener: Subscription<State> = | state: &State | {
            web_sys::console::log_1( &format!("Counter changed! New value: {}", state.counter).into() ) ;
            app.render();
        };

        store.borrow_mut().subscribe(listener);
        app
    }
chinedufn commented 4 years ago

Heads up I plan to address this issue over the weekend! Hang tight, sorry!

chinedufn commented 4 years ago

What is Subscription? Can you show me the definition for that?


In terms of requestAnimationFrame, I can update the example to show how to do it.

I'd like to rewrite the example using some of the things that I've learned in the last couple of years, so this can be part of that work stream.

aldebaranzbradaradjan commented 4 years ago

Hi, nice to see a response :)

So, Subscription is part of redux_rs crate, i think it works like your store in the example here : https://github.com/chinedufn/percy/blob/master/examples/isomorphic/app/src/store.rs and https://github.com/chinedufn/percy/blob/master/examples/isomorphic/client/src/lib.rs

In your example you do this :

    // TODO: Use request animation frame from web_sys
    // https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Window.html#method.request_animation_frame
        app.store.borrow_mut().subscribe(Box::new(|| {
            web_sys::console::log_1(&"Updating state".into());
            global_js.update();
        }));

The definition of Subscription : https://docs.rs/redux-rs/0.1.0/redux_rs/type.Subscription.html The definition force the use of a function ( fn(_: &State); ) but if i use a closure that catch scope variables it can't be converted as function, it's remain as a closure. So I can maybe call my render update with the subscription of redux_rs if I modify the crate, but I don't know how to do this. This is why I'm interested by your solution with request_animation_frame :)

I will wait for your example rewrite in this case ! Thanks !