frender
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
- 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:
Quick Start
-
Create a new cargo project
cargo new my-frender-app cd my-frender-app
-
Add
frender
to dependencies inCargo.toml
.[dependencies] frender = "= 1.0.0-alpha.8"
-
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>
-
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> ) } }
-
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>, } } }