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 withDebug
severity as minimum level.The line
review::render(App(()).into(), "root");
insidemain()
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 aVNode
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 theFrom<VNode>
trait)with_children
to attach a vector of child (every element should implement). reView provide achildren!
macro to simplify the creation of the child vectorwith_attribute
to attach an attribute specifing akey
and avalue
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 aEventType
and anEvent
. reView provide acallback!
macro to create anEvent
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 implementPartialEq
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.