frender

Crates.io docs.rs GitHub license GitHub stars

frender logo

Functional Rendering: React in Rust

frender is still in alpha and it's api might change. For now it is recommended to specify the exact version in Cargo.toml. Before updating, please see the full changelog in case there are breaking changes.

There are some example apps in examples folder. You can preview them at this site.

You can quick start with the frender book.

Features

  • Functional components and hooks
  • Statically typed props
  • Forced immutability with Rc

Future Development Plans

  • Clarify static text / uncached text / cached text for csr.
  • Documentation
  • Intrinsic svg components
  • Export frender components to js
  • Server Side Rendering
  • Type checking for CssProperties
  • Css-in-rust (For example, integrate with emotion/react)
  • Performance benchmarking

Contributing

frender is open sourced at GitHub. Pull requests and issues are welcomed.

You can also sponsor me and I would be very grateful :heart:

Buy Me a Coffee at ko-fi.com

Development

Files in crates/frender-html/src/html/props_builders are generated from mod props_builders in crates/frender-html/src/html.rs with the following command:

cargo run --bin frender-html-expand

Quick Start

  1. Create a new cargo project

    cargo new my-frender-app
    cd my-frender-app
    
  2. Add frender to dependencies in Cargo.toml.

    [dependencies]
    frender = "= 1.0.0-alpha.8"
    
  3. Create index.html in the project root directory.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>My frender App</title>
        <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
        <link data-trunk rel="rust" href="Cargo.toml" />
      </head>
      <body>
        <div id="frender-root"></div>
      </body>
    </html>
    
  4. Modify src/main.rs

    
    #![allow(unused)]
    fn main() {
    use frender::prelude::*;
    
    #[component(main(mount_element_id = "frender-root"))]
    fn Main() {
        rsx!(
            <div>
                "Hello, frender!"
            </div>
        )
    }
    }
    
  5. Run with trunk

    Install trunk and then execute:

    trunk serve
    

    Then you can navigate to http://localhost:8080 to see your frender app.

rsx syntax

Frender provides the rsx macro to create elements with syntax similar to jsx.

Create elements with props

rsx prop key should be an valid ident. Any literal string or expression wrapped in braces can be used as rsx prop value.


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent
        prop1="my-literal-string"
        prop2={my_expr}
        prop3
    />
)
}

The above code requires MyComponent to implements StaticComponent, and will be transformed into something like the following code, which makes props strongly typed.


#![allow(unused)]
fn main() {
MyComponent::create_element(
    MyComponent::Props::init_builder()
        .prop1("my-literal-string".into_prop_value())
        .prop2(my_expr.into_prop_value())
        .prop3(true.into_prop_value())
        .build()
)
}

Prop value auto conversion

Notice the my_expr.into_prop_value() in the above example. IntoPropValue::into_prop_value trait is introduced for auto converting prop values, so that rsx props can be less verbose.

For example, assuming the index prop of MyComponent accepts Option<i32>, you can create the element with any of the following codes.


#![allow(unused)]
fn main() {
rsx! ( <MyComponent index={Some(1)} /> )
rsx! ( <MyComponent index={1} /> )
}

IntoPropValue<Option<T>> is implemented for any type T. Thus, 1 can be converted to Some(1) automatically when used rsx prop value.

To disable the auto conversion, you can use := to set prop prop:={value}. For example:


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent prop1:="value" prop2="value" />
)
}

will be transformed into


#![allow(unused)]
fn main() {
MyComponent::create_element(
    MyComponent::Props::init_builder()
        .prop1("value")
        .prop2("value".into_prop_value())
        .build()
)
}

Create elements with children

Any literals, rsx elements or rust expressions wrapped in braces can be a valid child in rsx elements.

Elements with multiple children will accept a tuple of these values into children prop. For example:


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent>
        "my-literal-string"
        1
        {my_expr}
        <div id="my-div" />
    </MyComponent>
)
}

is equivalent to the following code:


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent
        children={
            (
                "my-literal-string",
                1,
                {my_expr},
                rsx!( <div id="my-div" /> ),
            )
        }
    />
)
}

Elements with exactly one child will accept a single value into children prop, not wrapped in a tuple. For example:


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent>
        <div id="my-div" />
    </MyComponent>
)
}

will be transformed to the following code:


#![allow(unused)]
fn main() {
rsx!(
    <MyComponent
        children={rsx!( <div id="my-div" /> )}
    />
)
}

Create intrinsic elements

Any component name starting with lower case letter [a-z] will be interpreted as an intrinsic component. For example, rsx!( <div id="my-div" /> ) will be resolved to:


#![allow(unused)]
fn main() {
use frender::prelude::*;
use self::intrinsic_components::div::prelude::*;

rsx! (
  <self::intrinsic_components::div::prelude::Component id="my-div" />
)
}

Notice the self::intrinsic_components. This means rsx searches for intrinsic_components module in the current module instead of sub module of frender. This would allow you to import a custom intrinsic_components in a module level.

Keyed element

key prop is preserved as a keyword, like React.js. When key prop is specified, the element will be wrapped with Keyed<TElement>, indicating the element is created with a key. For example:


#![allow(unused)]
fn main() {
use react::{Element, Keyed};

let element: Element = rsx!( <div /> );
let keyed_element: Keyed<Element> = rsx!( <div key="my-div" /> );
}

Fragment element


#![allow(unused)]
fn main() {
rsx!(
    <>
        "create"
        <i>"react"</i>
        "fragment"
        {"to wrap multiple children"}
    </>
)
}

The above rsx will be transformed into:


#![allow(unused)]
fn main() {
rsx!(
    <self::rsx_runtime::Fragment>
        "create"
        <i>"react"</i>
        "fragment"
        {"to wrap multiple children"}
    </self::rsx_runtime::Fragment>
)
}

To create fragment element with key, you can use # as the component name.


#![allow(unused)]
fn main() {
rsx!(
    <# key="my-key">1 2 3</#>
)
}

Note that Fragment component outputs a custom element type FragmentElement. It implements Into<react::Element>.

Enclose any element with </_>

You can enclose any element with </_>, which is very useful when the component type has a long module path or complex generics.


#![allow(unused)]
fn main() {
rsx!(
    <>
        <path::to::Component></_>
        // the above is equivalent to:
        <path::to::Component></path::to::Component>

        <MyComponent<i32>>1</_>
        // the above is equivalent to:
        <MyComponent<i32>>1</MyComponent<i32>>
    </>
)
}

Write a component

With the #[component] macro, writing a component in frender is as simple as writing a rust fn. The macro will turn the fn item to a struct, and implement UseRenderStatic and ComponentStatic for it. The fn body will be the body of UseRenderStatic::use_render method. The return type of the fn will be UseRenderStatic::RenderOutput. The optional first arg of the fn will be the props type of this component, which is UseRenderStatic::RenderArg and [ComponentStatic::Props]. If there is no args in the fn, the props type will be react::NoProps.

The return type can be omitted. If omitted, the default return type is react::Element.

Component with no props


#![allow(unused)]
fn main() {
use frender::prelude::*;

#[component]
fn MyComponent() {
  //            ^
  //            the return type defaults to react::Element
  rsx!( <div /> )
}

fn check_something() -> bool { true }

// Or you can specify the return type explicitly
#[component]
fn MyAnotherComponent() -> Option<react::Element> {
  if check_something() {
    Some(rsx!( <MyComponent /> ))
  } else {
    None
  }
}
}

Component with props

First, define props with def_props macro.


#![allow(unused)]
fn main() {
use frender::prelude::*;

def_props! {
  pub struct MyProps {
    // Required prop
    name: String,

    // Optional prop which defaults to `Default::default()`
    // The following property `age` is optional, and defaults to `None`
    age?: Option<u8>,

    // The following property `tags` is optional, and defaults to `Vec::default()`
    tags?: Vec<String>,

    // If the prop type is not specified,
    // then frender will infer the type by prop name.
    // For example, `class_name` default has type `Option<String>`
    // The following property `class_name` is optional, has type Option<String>
    class_name?,

    // The following property `id` is required, has type Option<String>
    id,

    // Prop can also have type generics.
    // For example, the following is
    // the default definition for prop `children`,
    // which means it accepts any `Option<TNode>` where TNode implements react::Node,
    // and then map the value into `Option<react::Children>` and store it into MyProps.
    children<TNode: react::Node>(value: Option<TNode>) -> Option<react::Children> {
      value.and_then(react::Node::into_children)
    },
  }
}
}

Then write the component with the above props:


#![allow(unused)]
fn main() {
use frender::prelude::*;

#[component]
pub fn MyComponent(props: &MyProps) {
    rsx!(
        <div>{&props.children}</div>
    )
}
}

Due to the generics, in some very rare cases, you may meet errors like type annotations needed cannot infer type for type parameter. You can solve it by specifying the type with the turbofish syntax ::<>. For example:


#![allow(unused)]
fn main() {
rsx! (
  // ERROR: type annotations needed
  <a children={None} />
)
rsx! (
  // it works!
  <a children={None::<()>} />
)
}

Hooks

React hooks are available in frender.

The documentation is working in progress. You can checkout the examples for the usage.

How Frender works

Frender is based on react's functional components (React.FC). React.FC are just js functions with special restrictions ( such as must return null | React.Element, must obey hooks rules). What frender do is just to convert a rust function to a js function with wasm_bindgen::clousre::Closure.

However, the problem is that js doesn't know about the ownership and lifetimes of data in rust. Luckily, React components have life cycles. In functional components, the best way to do tasks (side effects) in life cycles is using React.useEffect like the following.

function MyComponent() {
  React.useEffect(() => {
    // do side effects on mounted
    doSomething();

    return () => {
      // do side effects on unmounted
      doSomeCleanup();
    };
  }, []);
}

Frender bridges rust ownership within the component life cycle, by persisting some data when like the following:

function MyComponent() {
  const refInitialized = React.useRef(false);
  if (!refInitialized.current) {
    // persist rust data with
    // std::mem::ManuallyDrop
    persistRustData();
    refInitialized.current = true;
  }
  React.useEffect(() => {
    return () => {
      // manually drop the persisted data
      // on unmounted
      dropPersistedRustData();
    };
  }, []);
}

With the above idea, frender::react::use_ref can be implemented by guarding React.useRef with the following pseudo code.

fn use_ref<T>(initial_value: T) {
    let js_ref_object = React.useRef();
    if js_ref_object.is_undefined() {
        // forget the initial value and use a number to represent it
        let key: usize = forget_and_get_key(initial_value);
        // numbers are safe and easy to be converted to a js number
        js_ref_object.current = key;
    }

    React.useEffect(|| {
        // return a cleanup function so that
        // the forgotten data will be dropped after component unmounted
        return || {
            // manually drop the forgotten data
            manually_drop_by_key(key);
        }
    }, []);

    let rust_ref_object = FrenderUseRefObject {
        // to get the current data,
        // just get the current key,
        // and then get the data
        current: || get_by_key(js_ref_object.current),
        set_current: |new_value| {
            let old_key = js_ref_object.current;
            manually_drop_by_key(old_key);
            let new_key = forget_and_get_key(new_value);
            js_ref_object.current = new_key;
        },
    };

    rust_ref_object
}

Frender guards other hooks and the component function in the same way to keep the app memory safe.

Next, you can read about How rsx macro works.

How rsx macro works

As described in the guide, frender interprets rsx!( <MyComponent prop={value} /> ) as the builder pattern:

MyComponent::create_element(
    MyComponent::Props::init_builder()
        .prop(value)
        .build()
)

But how does the builder pattern know whether a prop is required or optional? Here I will explain the implementation details.

Completely optional props

If the properties of a props struct are all optional, it would be easy to implement the builder pattern. We can use the struct itself as the builder type.

struct MyProps {
    // defaults to None
    optional_name: Option<String>,
    // Optional prop does not need to be Option<T>.
    // It just need to implement Default.
    // The following prop defaults to 0.
    optional_num: i32
}

impl frender::react::Props for MyProps {
    type InitialBuilder = Self;

    fn init_builder() -> Self {
        Self {
            optional_name: Default::default(),
            optional_num: Default::default(),
        }
    }
}

// builder methods
impl MyProps {
    fn optional_name(mut self, value: Option<String>) -> Self {
        self.optional_name = value;
        self
    }

    fn optional_num(mut self, value: i32) -> Self {
        self.optional_num = value;
        self
    }
}

impl frender::react::PropsBuilder<MyProps> for MyProps {
    fn build(self) -> MyProps {
        self
    }
}

fn main() {
    let props = MyProps::init_builder()
        .optional_name("frender".to_string())
        .build();

    assert_eq!(props.optional_name, "frender");
    assert_eq!(props.optional_num, 0);
}

Props with required fields


#![allow(unused)]
fn main() {
struct PropertyAlreadySet<T>(T);
struct PropertyNotSet;

struct MyProps {
    // required property
    pub name: String,
    // optional property, defaults to None
    pub optional_message: Option<String>,
}

impl frender::react::Props for MyProps {
    type InitialBuilder = MyPropsBuilder;

    fn init_builder() -> MyPropsBuilder {
        MyPropsBuilder {
            // required field is initialized as `PropertyNotSet`
            name: PropertyNotSet,
            // optional field is initialized as default
            optional_message: Default::default(),
        }
    }
}

struct MyPropsBuilder<MyPropsBuilder__name> {
    name: MyPropsBuilder__name,
    optional_message: Option<String>,
}

impl<MyPropsBuilder__name> MyPropsBuilder<MyPropsBuilder__name> {
    pub fn name(self, value: String) -> MyPropsBuilder<PropertyAlreadySet<String>> {
        MyPropsBuilder {
            name: value,
            optional_message: self.optional_message,
        }
    }

    pub fn optional_message(mut self, value: Option<String>) -> Self {
        self.optional_message = value;
        self
    }
}

impl frender::react::PropsBuilder<MyProps> for MyPropsBuilder<PropertyAlreadySet<String>> {
    fn build(self) -> MyProps {
        MyProps {
            name: self.name.0,
            optional_message: self.optional_message,
        }
    }
}
}

def_props macro

Using frender, you don't need to implement the above yourself. All of the details is encapsulated in the def_props macro.


#![allow(unused)]
fn main() {
extern crate frender;
use frender::prelude::*;

def_props! {
    struct MyProps {
        // required property
        pub name: String,
        // optional property, defaults to None
        pub optional_message?: Option<String>,
    }
}
}