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
frendercomponents 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
frenderto dependencies inCargo.toml.[dependencies] frender = "= 1.0.0-alpha.8" -
Create
index.htmlin 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
trunkInstall trunk and then execute:
trunk serveThen you can navigate to
http://localhost:8080to 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>, } } }
