chinedufn / percy

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

Percy Hooks #94

Open cryptoquick opened 5 years ago

cryptoquick commented 5 years ago

Forking from the discussion in #28

Rationale

So, my quick pitch for hooks is, they are a great refinement on the state-management and side-effect management patterns necessary for developing React apps of all sizes and complexities, from small to large apps. They truly offer impressive clarity to React-style virtual DOM apps that I and many others appreciate. It's embraced with a great deal of positivity from the React community, and would be expected by the sort of early-adopters that would be curious enough to think of using Percy in a serious fashion.

I've also found, although Percy components come with a render method, I find myself creating composable stateless functional component analogs in Percy; i.e., functions like this:

fn form_input(store: Rc<RefCell<Store>>, name: Rc<RefCell<String>>) -> VirtualNode {
    let input_store: Rc<RefCell<Store>> = Rc::clone(&store);
    let value_store: Rc<RefCell<Store>> = Rc::clone(&store);
    let input_name: Rc<RefCell<String>> = Rc::clone(&name);
    let value_name: Rc<RefCell<String>> = Rc::clone(&name);

    html! {
        <input
            oninput=move |event: Event| {
                let input_elem = event.target().unwrap();
                let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
                let value: String = input_elem.value();
                let store: Rc<RefCell<Store>> = Rc::clone(&input_store);
                let name: Rc<RefCell<String>> = Rc::clone(&input_name);
                store.borrow_mut().msg(&Msg::Input(name.borrow().to_string(), value));
            }
            value=get_field(Rc::clone(&value_store), Rc::clone(&value_name))
        />
    }
}

As you can see, this pattern is rather unwieldy, and stores input values in global state, which could be good or bad, but in this case, I'd also need a clear form method to clear the values in this form for a user easily without requiring the user to refresh the page. It'd be best if, in many cases, like UI dropdowns, button states, and form inputs, for example.

The three I find most valuable are:

  1. useState - However, Rust has no means of creating an arbitrary positional destructuring of tuples, (at least, not yet*).
  2. useReducer
  3. useEffect

Examples

Much like React Hooks, I think it'd be good to come up with a similar implementation specific to Percy's virtual DOM implementation.

useState

Local component state management, kept track of in an internal vector indexed by render order.

TODO Add Rust-idiomatic example

useReducer

Used for more global state, which is more persistent throughout the render process. Important to prevent RefCell "prop drilling."

TODO Add Rust-idiomatic example

useEffect

This could be valuable for providing an easy escape hatch into JS functions like fetch and other things used in the wasm-bindgen, web_sys, and js_sys environment. It could provide a signal to the server/SSR to completely ignore code in this closure / macro.

TODO Add Rust-idiomatic example

cryptoquick commented 5 years ago

I'm also thinking of an additional Hook: useFetch, since it should be a common enough pattern. My major concern is that I want to provide request AND response Serde struct/models. Unfortunately, I don't think those could be provided as generics; pardon the fuzziness on that one, I'm still working on adding fetch to my app right now. Also, in the future, I really want to investigate GraphQL.

chinedufn commented 5 years ago

So the big thing on my mind is being hesitant to add things that people need to know.

I'm very into the idea of having the absolute minimum number of concepts to learn while maximizing productivity. So if a concept create a big boon in productivity, it's of course worth it, but I have some questions here.


For example, this to me seems like it can be written like

fn form_input(store: Rc<RefCell<Store>>, name: String) -> VirtualNode {
    let value: &str = store.get_whatever(&name);

    html! {
        <input
            oninput=move |event: Event| {
                let input_elem = event.target().unwrap();
                let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
                let val = input_elem.value();
                store.borrow_mut().msg(&Msg::Input(name, val));
            }
            value=value
        />
    }
}

To me it's very simple to just have one single global state object that can only be mutated using msg.

This gives the developer the absolute minimal number of concepts to know without, IMO, introducing any material downsides.

Even when I write React at work I advocate heavily for just having a single global state that gets updated by reducers. No local state except unless there's an extremely compelling reason (happens occasionally).

I think that applications are simpler when there is only one way to do things (when possible).


Curious about your thoughts here - but in short I'm not totally convinced that introducing 4 new concepts provides much value. The caveat here is that I haven't used hooks so I might just be blind to the value - so open to your convincing!!

cryptoquick commented 5 years ago

You're not wrong on many of these aspects, and I'd like some time to build on my project to help me come to clarity on what's necessary to simplify this code.

The code you provided is what I started with, but I had a lot of trouble fighting with the borrow-checker as I started pulling things out into their own functions.

chinedufn commented 5 years ago

Gotcha gotcha - that approach sounds great to me