reView Book

Welcome to the reView Book, a simple documentation about reView.

What is reView?

reView is a library written in Rust for creating front-end web app using WebAssembly.

reView is not production-ready, and it's a WIP project so expect breaking changes between versions.

Project Setup

Installing Rust

To install Rust, follow the official instructions.

Rust can compile source codes for different "targets" (e.g. different processors). The compilation target for browser-based WebAssembly is called "wasm32-unknown-unknown". The following command will add this target to your development environment.

rustup target add wasm32-unknown-unknown

Install Trunk

Trunk is a great tool for managing deployment and packaging, and will be the default choice for reView. It will be used in the documentation, in every example and in the default review-template.

# note that this might take a while to install, because it compiles everything from scratch
# Trunk also provides prebuilt binaries for a number of major package managers
# See https://trunkrs.dev/#install for further details
cargo install trunk wasm-bindgen-cli

Summary

Now that you have all the tools needed, we can build a sample application.

Build a sample app

Create a project using the default template

To get started, if you haven't already done it, install cargo generate.

cargo install cargo-generate

Now you run

cargo generate --git https://github.com/malpenzibo/review-template

go through the wizard specifing the project name and than enter in the new created folder and run

trunk serve

If everithing goes well you can see the result on localhost:8080

Create a project from scratch

To get started, create a new project running

cargo new review-app

and open the newly created directory.

cd review-app

Now, update Cargo.toml adding reView as dependencies.

[package]
name = "review-app"
version = "0.1.0"
edition = "2021"

[dependencies]
review = "0.1.0"

Update main.rs

We need to generate a component called App which renders a button that updates it's value when clicked.

Replace the contents of src/main.rs with the following code.

use review::Tag::{Button, Div};
use review::EventType::OnClick;
use review::{callback, children, component, use_state, ElementBuilder};

#[component(App)]
pub fn app() -> VNode {
    let (state, set_state) = use_state(0);

    Div.with_children(children!(
        format!("Current value {}", state),
        Button
            .with_child("Increase counter")
            .with_event(OnClick, callback!(move || { set_state(*state + 1) }))
    ))
    .into()
}

fn main() {
    review::init_logger(review::log::Level::Debug);

    review::render(App(()).into(), "root");
}

NOTE

The line review::init_logger(review::log::Level::Debug); will initialize the log system with Debug severity as minimum level.

The line review::render(App(()).into(), "root"); inside main() starts your application and mounts it inside the element with the "root" id.

Create index.html

Finally, add an index.html file in the root directory of your app.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>reView App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

View your web application

Run the following command to build and serve the application locally.

trunk serve

Trunk will helpfully rebuild your application if you modify any of its files.

Congratulations

You have now successfully setup your reView development environment, and built your first web application and could see the result here localhost:8080

Concepts

reView is a simple library to create single page application. The API itself is similar to the react API and for this reason some concepts like hooks and functional components are similar.

To minimalize the DOM manipulation reView use a virtual DOM that is synchronized with the real DOM. Every node in the virtual DOM is represented by a VNode that will be materialized in a real DOM element only when required.

There is 3 kind of VNode:

  • a Element that represent a standard DOM element and corresponds to a DOM node created with createElement (eg: div, p, button, etc...)
  • a Text that represent a simple string displayed in the DOM and corresponds to a DOM node created with createTextNode
  • a Component that is a main reView building block represented by a function that returns a VNode

Element Builder API

reView doesn't have a macro to support a syntax similar to jsx but instead provides an API to create the UI.

The main concept is that every Tag and every String could become a VNode.

For this reason both Tag, String and VElement implement the From<VNode> trait to obtain a VNode using into().

To attach attributes or children to a Tag element we could use a builder API.

Every Tag or VElement implements the ElementBuilder trait so we can call:

  • with_child to attach a single child (the child should implement the From<VNode> trait)
  • with_children to attach a vector of child (every element should implement). reView provide a children! macro to simplify the creation of the child vector
  • with_attribute to attach an attribute specifing akey and a value
  • with_attributes to attach a vector of attributes. A vector of attribute is a vector of tuple (key, value)
  • with_event to attach an event specifing a EventType and an Event. reView provide a callback! macro to create an Event from a rust closure.

Using this API we can create a customized VElement that could be converted into a VNode with into().

Main.with_children(children!(
    Img.with_attribute("class", "logo")
        .with_attributes(vec!(
            ("src", "/assets/logo.png"), 
            ("alt", "reView logo")
        ))
        .with_event(OnClick, callback!(|| { log::info!("hello!!") }))
    H1.with_child("Hello World!"),
    Span.with_attribute("class", "subtitle")
        .with_children(children!(
            "from reView with ",
            I.with_attribute("class", "heart")
        ))
))
.into()

Components

A Component consist of a single function that receives props and determines what should be rendered by returning a VNode. Without hooks a component is quite limiting because can only be a pure component. Hooks allow components to maintain their own internal state and use other reView features.

Creating a Components

A component can be created using the #[component] attribute on top of a function.

#[component(ExampleComponent)]
pub fn example_component() -> VNode {
    Div.into()
}

Under the hood

A functional component is a struct that implements the ComponentProvider trait. This trait has two methods, the render method and the get_props method. The first one is used to retrieve the VNode produced by the component and the second one il used to get a reference to the props that should be send to the render method.

The component attribute will automatically create a struct that implement the ComponentProvider using the specified function. Also checks that the fuction respect the hook rules.

Hooks

Hooks are functions that let you store state and perform side-effects.

reView comes with a few pre-defined Hooks. You can also create your own custom hooks.

Rules of hooks

  • A hook function name always has to start with use_
  • Hooks can only be used at the following locations:
    • Top level of a function / hook.
    • If condition inside a function / hook, given it's not already branched.
    • Match condition inside a function / hook, given it's not already branched.
    • Blocks inside a function / hook, given it's not already branched.
  • Every render must call the hooks in the same order

All these rules are enforced by either compile time or run-time errors.

Pre-defined Hooks

reView comes with the following predefined Hooks:

State Hook

use_state is used to manage state in a component. It returns a State<T> that is a tuple with the first element that is a Rc<T> with the current stored values and as second element an Rc<Fn(T)> to change the current stored values.

The hook takes a value as input which determines the initial value.

Example

#[component(StateExample)]
fn state_example() -> VNode {
    let (state, set_state) = use_state(0);

    Div.with_children(children!(
        format!("Current value {}", state),
        Button
            .with_child("Increase counter")
            .with_event(OnClick, callback!(move || { set_state(*state + 1) }))
    ))
    .into()
}

Effect Hook

use_effect is used for hooking into the component's lifecycle and creating side-effects.

It takes a function which is called every time after the component's render has finished.

This function returns an Optional closure that will be called after the execution of the effect. This optional closure is a cleanup function and it's usefull if is necessary to perform some cleanups after the execution of the effect.

The use_effect hooks accept, as second optional argument, the effect dependencies. Only when the dependencies change, it calls the provided function.

Note

dependencies must implement PartialEq

Examples

Without cleanup and dependencies

#[component(StateExample)]
fn state_example() -> VNode {
    let (state, set_state) = use_state(0);

    use_effect(
        || {
            review::log::info!("hello world!!");
            None::<fn()>
        },
        None::<()>,
    );

    Div.with_children(children!(
        format!("Current value {}", state),
        Button
            .with_child("Increase counter")
            .with_event(OnClick, callback!(move || { set_state(*state + 1) }))
    ))
    .into()
}

With dependencies and without cleanup

#[component(StateExample)]
fn state_example() -> VNode {
    let (state, set_state) = use_state(0);

    use_effect(
        || {
            review::log::info!("hello world!!");
            None::<fn()>
        },
        Some(*state),
    );

    Div.with_children(children!(
        format!("Current value {}", state),
        Button
            .with_child("Increase counter")
            .with_event(OnClick, callback!(move || { set_state(*state + 1) }))
    ))
    .into()
}

With dependencies and cleanup

#[component(StateExample)]
fn state_example() -> VNode {
    let (state, set_state) = use_state(0);

    use_effect(
        || {
            log::info!("hello world!!");
            Some(|| log::info!("cleanup"))
        },
        Some(*state),
    );

    Div.with_children(children!(
        format!("Current value {}", state),
        Button
            .with_child("Increase counter")
            .with_event(OnClick, callback!(move || { set_state(*state + 1) }))
    ))
    .into()
}

Custom Hook

It's possible to define custom Hooks using multiple standard hook inside a single function.

For example if we want to log every state change we could create a custom hook like this:

#[hook]
pub fn use_traced_state<T>(init_value: T) -> State<T>
where
    T: Any + PartialEq + Debug + Display,
{
    let (state, set_state) = use_state(init_value);

    use_effect(
        {
            let state = state.clone();
            move || {
                log::info!("{}", state);
                None::<fn()>
            }
        },
        Some(state.clone()),
    );

    (state, set_state)
}

In order to use the default hooks and become a custom hook a function should have the hook attribute.