wip: remove runner on hook and then update docs
This commit is contained in:
1 parent
7d41a16208
commit
d1560450ba
95 files changed
+1339
-675
No files matched your search
@@ -0,0 +1,73 @@
|
||||
# Core Topics
|
||||
|
||||
In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app.
|
||||
|
||||
At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes.
|
||||
|
||||
1) We declare what we want our user interface to look like given a state using Rust-based logic and control flow.
|
||||
2) We declare how we want our state to change when the user triggers an event.
|
||||
|
||||
## Declarative UI
|
||||
|
||||
Dioxus is a *declarative* framework. This means that instead of manually writing calls to "create element" and "set element background to red," we simply *declare* what we want the element to look like and let Dioxus handle the differences.
|
||||
|
||||
Let's pretend that we have a stoplight we need to control - it has a color state with red, yellow, and green as options.
|
||||
|
||||
|
||||
Using an imperative approach, we would have to manually declare each element and then handlers for advancing the stoplight.
|
||||
|
||||
```rust
|
||||
let container = Container::new();
|
||||
|
||||
let green_light = Light::new().color("green").enabled(true);
|
||||
let yellow_light = Light::new().color("yellow").enabled(false);
|
||||
let red_light = Light::new().color("red").enabled(false);
|
||||
container.push(green_light);
|
||||
container.push(yellow_light);
|
||||
container.push(red_light);
|
||||
|
||||
container.set_onclick(move |_| {
|
||||
if red_light.enabled() {
|
||||
red_light.set_enabled(false);
|
||||
green_light.set_enabled(true);
|
||||
} else if yellow_light.enabled() {
|
||||
yellow_light.set_enabled(false);
|
||||
red_light.set_enabled(true);
|
||||
} else if green_light.enabled() {
|
||||
green_light.set_enabled(false);
|
||||
yellow_light.set_enabled(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
As the UI grows in scale, our logic to keep each element in the proper state would grow exponentially. This can become very unwieldy and lead to out-of-sync UIs that harm user experience.
|
||||
|
||||
Instead, with Dioxus, we *declare* what we want our UI to look like:
|
||||
|
||||
```rust
|
||||
let mut state = use_state(&cx, || "red");
|
||||
|
||||
cx.render(rsx!(
|
||||
Container {
|
||||
Light { color: "red", enabled: state == "red", }
|
||||
Light { color: "yellow", enabled: state == "yellow", }
|
||||
Light { color: "green", enabled: state == "green", }
|
||||
|
||||
onclick: move |_| {
|
||||
state.set(match *state {
|
||||
"green" => "yellow",
|
||||
"yellow" => "red",
|
||||
"red" => "green",
|
||||
})
|
||||
}
|
||||
}
|
||||
))
|
||||
```
|
||||
|
||||
Remember: this concept is not new! Many frameworks are declarative - with React being the most popular. Declarative frameworks tend to be much more enjoyable to work with than imperative frameworks.
|
||||
|
||||
Here's some reading about declaring UI in React:
|
||||
|
||||
- [https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js](https://stackoverflow.com/questions/33655534/difference-between-declarative-and-imperative-in-react-js)
|
||||
|
||||
- [https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44](https://medium.com/@myung.kim287/declarative-vs-imperative-251ce99c6c44)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Subscriptions
|
||||
|
||||
Yew subscriptions are used to schedule update for components into the future. The `Context` object can create subscriptions:
|
||||
|
||||
```rust
|
||||
fn Component(cx: Component) -> DomTree {
|
||||
let update = cx.schedule();
|
||||
|
||||
// Now, when the subscription is called, the component will be re-evaluated
|
||||
update.consume();
|
||||
}
|
||||
```
|
||||
|
||||
Whenever a component's subscription is called, the component will then re-evaluated. You can consider the input properties of
|
||||
a component to be just another form of subscription. By default, the Dioxus component system automatically diffs a component's props
|
||||
when the parent function is called, and if the props are different, the child component's subscription is called.
|
||||
|
||||
The subscription API exposes this functionality allowing hooks and state management solutions the ability to update components whenever
|
||||
some state or event occurs outside of the component. For instance, the `use_context` hook uses this to subscribe components that use a
|
||||
particular context.
|
||||
|
||||
```rust
|
||||
fn use_context<I>(cx: Scope<T>) -> I {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# Concurrent mode
|
||||
|
||||
Concurrent mode provides a mechanism for building efficient asynchronous components. With this feature, components don't need to render immediately, and instead can schedule a future render by returning a future.
|
||||
|
||||
To make a component asynchronous, simply change its function signature to async.
|
||||
|
||||
```rust
|
||||
fn Example(cx: Scope) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Scope) -> Vnode {
|
||||
rsx!{ <div> "Hello world!" </div> }
|
||||
}
|
||||
```
|
||||
|
||||
Now, logic in components can be awaited to delay updates of the component and its children. Like so:
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Scope) -> Vnode {
|
||||
let name = fetch_name().await;
|
||||
rsx!{ <div> "Hello {name}" </div> }
|
||||
}
|
||||
|
||||
async fetch_name() -> String {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This component will only schedule its render once the fetch is complete. However, we _don't_ recommend using async/await directly in your components.
|
||||
|
||||
Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion.
|
||||
|
||||
Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks.
|
||||
|
||||
As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading.
|
||||
|
||||
```rust
|
||||
async fn ExampleLoader(cx: Scope) -> Vnode {
|
||||
/*
|
||||
Fetch, pause the component from rendering at all.
|
||||
|
||||
The component is locked while waiting for the request to complete
|
||||
While waiting, an alternate component is scheduled in its place.
|
||||
|
||||
This API stores the result on the Context object, so the loaded data is taken as reference.
|
||||
*/
|
||||
let name: &Result<SomeStructure> = use_fetch_data("http://example.com/json", ())
|
||||
.place_holder(|cx| rsx!{<div> "loading..." </div>})
|
||||
.delayed_place_holder(1000, |cx| rsx!{ <div> "still loading..." </div>})
|
||||
.await;
|
||||
|
||||
match name {
|
||||
Ok(name) => rsx! { <div> "Hello {something}" </div> },
|
||||
Err(e) => rsx! { <div> "An error occurred :(" </div>}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
async fn Example(cx: Scope) -> DomTree {
|
||||
// Diff this set between the last set
|
||||
// Check if we have any outstanding tasks?
|
||||
//
|
||||
// Eventually, render the component into the VDOM when the future completes
|
||||
<div>
|
||||
<Example />
|
||||
</div>
|
||||
|
||||
// Render a div, queue a component
|
||||
// Render the placeholder first, then when the component is ready, then render the component
|
||||
<div>
|
||||
<Suspense placeholder={html!{<div>"Loading"</div>}}>
|
||||
<Example />
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# Memoization and the arena allocator
|
||||
|
||||
Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator.
|
||||
|
||||
One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components.
|
||||
|
||||
With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will _not_ cascade to children if the child's props did not change.
|
||||
|
||||
https://dmitripavlutin.com/use-react-memo-wisely/
|
||||
|
||||
This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization.
|
||||
|
||||
```rust
|
||||
fn test() -> DomTree {
|
||||
html! {
|
||||
<>
|
||||
<SomeComponent nomemo />
|
||||
// same as
|
||||
<SomeComponent nomemo=true />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
static TestComponent: Component = |cx| html!{<div>"Hello world"</div>};
|
||||
|
||||
static TestComponent: Component = |cx|{
|
||||
let g = "BLAH";
|
||||
html! {
|
||||
<div> "Hello world" </div>
|
||||
}
|
||||
};
|
||||
|
||||
#[inline_props]
|
||||
fn test_component(cx: Scope, name: String) -> Element {
|
||||
rsx!(cx, "Hello, {name}")
|
||||
}
|
||||
```
|
||||
|
||||
## Why this behavior?
|
||||
|
||||
"This is different than React, why differ?".
|
||||
|
||||
Take a component likes this:
|
||||
|
||||
```rust
|
||||
fn test(cx: Scope) -> DomTree {
|
||||
let Bundle { alpha, beta, gamma } = use_context::<SomeContext>(cx);
|
||||
html! {
|
||||
<div>
|
||||
<Component name=alpha />
|
||||
<Component name=beta />
|
||||
<Component name=gamma />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While the contents of the destructured bundle might change, not every child component will need to be re-rendered.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Signals: Skipping the Diff
|
||||
|
||||
In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant.
|
||||
|
||||
Signals build on the same infrastructure that powers asynchronous rendering where in-tree values can be updated outside of the render phase. In async rendering, a future is used as the signal source. With the raw signal API, any value can be used as a signal source.
|
||||
|
||||
By default, Dioxus will only try to diff subtrees of components with dynamic content, automatically skipping diffing static content.
|
||||
|
||||
## What does this look like?
|
||||
|
||||
Your component today might look something like this:
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let (title, set_title) = use_state(&cx, || "Title".to_string());
|
||||
cx.render(rsx!{
|
||||
input {
|
||||
value: title,
|
||||
onchange: move |new| set_title(new.value())
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This component is fairly straightforward - the input updates its own value on every change. However, every call to set_title will re-render the component. If we add a large list, then every time we update the title input, Dioxus will need to diff the entire list, over, and over, and over. This is **a lot** of wasted clock-cycles!
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let (title, set_title) = use_state(&cx, || "Title".to_string());
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
input {
|
||||
value: title,
|
||||
onchange: move |new| set_title(new.value())
|
||||
}
|
||||
ul {
|
||||
{0..10000.map(|f| rsx!{
|
||||
li { "{f}" }
|
||||
})}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Many experienced React developers will just say "this is bad design" - but we consider it to be a pit of failure, rather than a pit of success! That's why signals exist - to push you in a more performant (and ergonomic) direction. Signals let us directly bind values to their final place in the VirtualDOM. Whenever the signal value is updated, Dioxus will only the DOM nodes where that signal is used. Signals are built into Dioxus, so we can directly bind attributes of elements to their updates.
|
||||
|
||||
We can use signals to generate a two-way binding between data and the input box. Our text input is now just a two-line component!
|
||||
|
||||
```rust
|
||||
fn Comp(cx: Scope) -> DomTree {
|
||||
let mut title = use_signal(&cx, || String::from("Title"));
|
||||
cx.render(rsx!(input { value: title }))
|
||||
}
|
||||
```
|
||||
|
||||
For a slightly more interesting example, this component calculates the sum between two numbers, but totally skips the diffing process.
|
||||
|
||||
```rust
|
||||
fn Calculator(cx: Scope) -> DomTree {
|
||||
let mut a = use_signal(&cx, || 0);
|
||||
let mut b = use_signal(&cx, || 0);
|
||||
let mut c = a + b;
|
||||
rsx! {
|
||||
input { value: a }
|
||||
input { value: b }
|
||||
p { "a + b = {c}" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex that a basic operation (`std::ops`) we can either chain signals together or combine them:
|
||||
|
||||
```rust
|
||||
let mut a = use_signal(&cx, || 0);
|
||||
let mut b = use_signal(&cx, || 0);
|
||||
|
||||
// Chain signals together using the `with` method
|
||||
let c = a.with(b).map(|(a, b)| *a + *b);
|
||||
```
|
||||
|
||||
## Deref and DerefMut
|
||||
|
||||
If we ever need to get the value out of a signal, we can simply `deref` it.
|
||||
|
||||
```rust
|
||||
let mut a = use_signal(&cx, || 0);
|
||||
let c = *a + *b;
|
||||
```
|
||||
|
||||
Calling `deref` or `derefmut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work.
|
||||
|
||||
## Global Signals
|
||||
|
||||
Sometimes you want a signal to propagate across your app, either through far-away siblings or through deeply-nested components. In these cases, we use Dirac: Dioxus's first-class state management toolkit. Dirac atoms automatically implement the Signal API. This component will bind the input element to the `TITLE` atom.
|
||||
|
||||
```rust
|
||||
const TITLE: Atom<String> = || "".to_string();
|
||||
const Provider: Component = |cx|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
rsx!(cx, input { value: title })
|
||||
};
|
||||
```
|
||||
|
||||
If we use the `TITLE` atom in another component, we can cause updates to flow between components without calling render or diffing either component trees:
|
||||
|
||||
```rust
|
||||
const Receiver: Component = |cx|{
|
||||
let title = use_signal(&cx, &TITLE);
|
||||
log::info!("This will only be called once!");
|
||||
rsx!(cx,
|
||||
div {
|
||||
h1 { "{title}" }
|
||||
div {}
|
||||
footer {}
|
||||
}
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
Dioxus knows that the receiver's `title` signal is used only in the text node, and skips diffing Receiver entirely, knowing to update _just_ the text node.
|
||||
|
||||
If you build a complex app on top of Dirac, you'll likely notice that many of your components simply won't be diffed at all. For instance, our Receiver component will never be diffed once it has been mounted!
|
||||
|
||||
## Signals and Iterators
|
||||
|
||||
Sometimes you want to use a collection of items. With Signals, you can bypass diffing for collections - a very powerful technique to avoid re-rendering on large collections.
|
||||
|
||||
By default, Dioxus is limited when you use iter/map. With the `For` component, you can provide an iterator and a function for the iterator to map to.
|
||||
|
||||
Dioxus automatically understands how to use your signals when mixed with iterators through Deref/DerefMut. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster.
|
||||
|
||||
```rust
|
||||
const DICT: AtomFamily<String, String> = |_| {};
|
||||
const List: Component = |cx|{
|
||||
let dict = use_signal(&cx, &DICT);
|
||||
cx.render(rsx!(
|
||||
ul {
|
||||
For { each: dict, map: |k, v| rsx!( li { "{v}" }) }
|
||||
}
|
||||
))
|
||||
};
|
||||
```
|
||||
|
||||
## Remote Signals
|
||||
|
||||
Apps that use signals will enjoy a pleasant hybrid of server-side and client-side rendering.
|
||||
|
||||
```rust
|
||||
|
||||
```
|
||||
|
||||
## How does it work?
|
||||
|
||||
Signals internally use Dioxus' asynchronous rendering infrastructure to perform updates out of the tree.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Subtrees
|
||||
|
||||
One way of extending the Dioxus VirtualDom is through the use of "Subtrees." Subtrees are chunks of the VirtualDom tree distinct from the rest of the tree. They still participate in event bubbling, diffing, etc, but will have a separate set of edits generated during the diff phase.
|
||||
|
||||
For a VirtualDom that has a root tree with two subtrees, the edits follow a pattern of:
|
||||
|
||||
Root
|
||||
-> Tree 1
|
||||
-> Tree 2
|
||||
-> Original root tree
|
||||
|
||||
- Root edits
|
||||
- Tree 1 Edits
|
||||
- Tree 2 Edits
|
||||
- Root Edits
|
||||
|
||||
The goal of this functionality is to enable things like Portals, Windows, and inline alternative renderers without needing to spin up a new VirtualDom.
|
||||
|
||||
With the right renderer plugins, a subtree could be rendered as anything - a 3D scene, SVG, or even as the contents of a new window or modal. This functionality is similar to "Portals" in React, but much more "renderer agnostic." Portals, by nature, are not necessarily cross-platform and rely on renderer functionality, so it makes sense to abstract their purpose into the subtree concept.
|
||||
|
||||
The desktop renderer comes pre-loaded with the window and notification subtree plugins, making it possible to render subtrees into entirely different windows.
|
||||
|
||||
Subtrees also solve the "bridging" issues in React where two different renderers need two different VirtualDoms to work properly. In Dioxus, you only ever need one VirtualDom and the right renderer plugins.
|
||||
|
||||
|
||||
## API
|
||||
|
||||
Due to their importance in the hierarchy, Components - not nodes - are treated as subtree roots.
|
||||
|
||||
|
||||
```rust
|
||||
|
||||
fn Subtree<P>(cx: Scope<P>) -> DomTree {
|
||||
|
||||
}
|
||||
|
||||
fn Window() -> DomTree {
|
||||
Subtree {
|
||||
onassign: move |e| {
|
||||
// create window
|
||||
}
|
||||
children()
|
||||
}
|
||||
}
|
||||
|
||||
fn 3dRenderer -> DomTree {
|
||||
Subtree {
|
||||
onassign: move |e| {
|
||||
// initialize bevy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# VNodes with RSX, HTML, and NodeFactory
|
||||
|
||||
Many modern frameworks provide a domain-specific-language for declaring user-interfaces. In the case of React, this language extension is called JSX and must be handled through additional dependencies and pre/post processors to transform your source code. With Rust, we can simply provide a procedural macro in the Dioxus dependency itself that mimics the JSX language.
|
||||
|
||||
With Dioxus, we actually ship two different macros - a macro that mimics JSX (the `html!` macro) and a macro that mimics Rust's native nested-struct syntax (the `rsx!` macro). These macros simply transform their inputs into NodeFactory calls.
|
||||
|
||||
For instance, this html! call:
|
||||
```rust
|
||||
html!(<div> "hello world" </div>)
|
||||
```
|
||||
becomes this NodeFactory call:
|
||||
```rust
|
||||
|f| f.element(
|
||||
dioxus_elements::div, // tag
|
||||
[], // listeners
|
||||
[], // attributes
|
||||
[f.static_text("hello world")], // children
|
||||
None // key
|
||||
)
|
||||
```
|
||||
The NodeFactory API is fairly ergonomic, making it a viable option to use directly. The NodeFactory API is also compile-time correct and has incredible syntax highlighting support. We use what Rust calls a "unit type" - the `dioxus_elements::div` and associated methods to ensure that a `div` can only have attributes associated with `div`s. This lets us tack on relevant documentation, autocomplete support, and jump-to-definition for methods and attributes.
|
||||
|
||||

|
||||
|
||||
## html! macro
|
||||
|
||||
The html! macro supports a limited subset of the html standard. Rust's macro parsing tools are somewhat limited, so all text between tags _must be quoted_.
|
||||
|
||||
However, writing HTML by hand is a bit tedious - IDE tools for Rust don't support linting/autocomplete/syntax highlighting. We suggest using RSX - it's more natural for Rust programs and _does_ integrate well with Rust IDE tools.
|
||||
|
||||
```rust
|
||||
let name = "jane";
|
||||
let pending = false;
|
||||
let count = 10;
|
||||
|
||||
dioxus::ssr::render_lazy(html! {
|
||||
<div>
|
||||
<p> "Hello, {name}!" </p>
|
||||
<p> "Status: {pending}!" </p>
|
||||
<p> "Count {count}!" </p>
|
||||
</div>
|
||||
});
|
||||
```
|
||||
|
||||
## rsx! macro
|
||||
|
||||
The rsx! macro is a VNode builder macro designed especially for Rust programs. Writing these should feel very natural, much like assembling a struct. VSCode also supports these with code folding, bracket-tabbing, bracket highlighting, section selecting, inline documentation, GOTO definition, and refactoring support.
|
||||
|
||||
When helpful, the Dioxus VSCode extension provides a way of converting a selection of HTML directly to RSX, so you can import templates from the web directly into your existing app.
|
||||
|
||||
It's also a bit easier on the eyes than HTML.
|
||||
|
||||
```rust
|
||||
dioxus::ssr::render_lazy(rsx! {
|
||||
div {
|
||||
p {"Hello, {name}!"}
|
||||
p {"Status: {pending}!"}
|
||||
p {"Count {count}!"}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
In the next section, we'll cover the `rsx!` macro in more depth.
|
||||
@@ -0,0 +1,229 @@
|
||||
# RSX in Depth
|
||||
|
||||
The RSX macro makes it very easy to assemble complex UIs with a very natural Rust syntax:
|
||||
|
||||
|
||||
```rust
|
||||
rsx!(div {
|
||||
button {
|
||||
"Add todo",
|
||||
onclick: move |e| todos.write().new_todo()
|
||||
}
|
||||
ul {
|
||||
class: "todo-list"
|
||||
(todos.iter().map(|(key, todo)| rsx!(
|
||||
li {
|
||||
class: "beautiful-todo"
|
||||
key: "f"
|
||||
h3 { "{todo.title}" }
|
||||
p { "{todo.contents}"}
|
||||
}
|
||||
)))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
In this section, we'll cover the `rsx!` macro in depth. If you prefer to learn through examples, the `reference` guide has plenty of examples on how to use `rsx!` effectively.
|
||||
|
||||
|
||||
|
||||
### Element structure
|
||||
|
||||
```rust
|
||||
div {
|
||||
hidden: false,
|
||||
"some text"
|
||||
child {}
|
||||
Component {}
|
||||
{/* literal tokens that resolve to vnodes */}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Each element takes a comma-separated list of expressions to build the node. Roughly, here's how they work:
|
||||
|
||||
- `name: value` sets a property on this element.
|
||||
- `"text"` adds a new text element
|
||||
- `tag {}` adds a new child element
|
||||
- `CustomTag {}` adds a new child component
|
||||
- `{expr}` pastes the `expr` tokens literally. They must be `IntoIterator<T> where T: IntoVnode` to work properly
|
||||
|
||||
Commas are entirely optional, but might be useful to delineate between elements and attributes.
|
||||
|
||||
The `render` function provides an **extremely efficient** allocator for VNodes and text, so try not to use the `format!` macro in your components. Rust's default `ToString` methods pass through the global allocator, but all text in components is allocated inside a manually-managed Bump arena. To push you in the right direction, all text-based attributes take `std::fmt::Arguments` directly, so you'll want to reach for `format_args!` when the built-in `f-string` interpolation just doesn't cut it.
|
||||
|
||||
|
||||
### Ignoring `cx.render` with `rsx!(cx, ...)`
|
||||
|
||||
Sometimes, writing `cx.render` is a hassle. The `rsx! macro will accept any token followed by a comma as the target to call "render" on:
|
||||
|
||||
```rust
|
||||
cx.render(rsx!( div {} ))
|
||||
// becomes
|
||||
rsx!(cx, div {})
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
Sometimes, you might not want to render an element given a condition. The rsx! macro will accept any tokens directly contained with curly braces, provided they resolve to a type that implements `IntoIterator<VNode>`. This lets us write any Rust expression that resolves to a VNode:
|
||||
|
||||
|
||||
```rust
|
||||
rsx!({
|
||||
if enabled {
|
||||
rsx!(cx, div {"enabled"})
|
||||
} else {
|
||||
rsx!(cx, li {"disabled"})
|
||||
}
|
||||
})
|
||||
```
|
||||
A convenient way of hiding/showing an element is returning an `Option<VNode>`. When combined with `and_then`, we can succinctly control the display state given some boolean:
|
||||
|
||||
```rust
|
||||
rsx!({
|
||||
a.and_then(rsx!(div {"enabled"}))
|
||||
})
|
||||
```
|
||||
|
||||
It's important to note that the expression `rsx!()` is typically lazy - this expression must be _rendered_ to produce a VNode. When using match statements, we must render every arm as to avoid the `no two closures are identical` rule that Rust imposes:
|
||||
|
||||
```rust
|
||||
// this will not compile!
|
||||
match case {
|
||||
true => rsx!(div {}),
|
||||
false => rsx!(div {})
|
||||
}
|
||||
|
||||
// the nodes must be rendered first
|
||||
match case {
|
||||
true => rsx!(cx, div {}),
|
||||
false => rsx!(cx, div {})
|
||||
}
|
||||
```
|
||||
|
||||
### Lists
|
||||
|
||||
Again, because anything that implements `IntoIterator<VNode>` is valid, we can use lists directly in our `rsx!`:
|
||||
|
||||
```rust
|
||||
let items = vec!["a", "b", "c"];
|
||||
|
||||
cx.render(rsx!{
|
||||
ul {
|
||||
{items.iter().map(|f| rsx!(li { "a" }))}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Sometimes, it makes sense to render VNodes into a list:
|
||||
|
||||
```rust
|
||||
let mut items = vec![];
|
||||
|
||||
for _ in 0..5 {
|
||||
items.push(rsx!(cx, li {} ))
|
||||
}
|
||||
|
||||
rsx!(cx, {items} )
|
||||
```
|
||||
|
||||
#### Lists and Keys
|
||||
|
||||
When rendering the VirtualDom to the screen, Dioxus needs to know which elements have been added and which have been removed. These changes are determined through a process called "diffing" - an old set of elements is compared to a new set of elements. If an element is removed, then it won't show up in the new elements, and Dioxus knows to remove it.
|
||||
|
||||
However, with lists, Dioxus does not exactly know how to determine which elements have been added or removed if the order changes or if an element is added or removed from the middle of the list.
|
||||
|
||||
In these cases, it is vitally important to specify a "key" alongside the element. Keys should be persistent between renders.
|
||||
|
||||
```rust
|
||||
fn render_list(cx: Scope, items: HashMap<String, Todo>) -> DomTree {
|
||||
rsx!(cx, ul {
|
||||
{items.iter().map(|key, item| {
|
||||
li {
|
||||
key: key,
|
||||
h2 { "{todo.title}" }
|
||||
p { "{todo.contents}" }
|
||||
}
|
||||
})}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
There have been many guides made for keys in React, so we recommend reading up to understand their importance:
|
||||
|
||||
- [React guide on keys](https://reactjs.org/docs/lists-and-keys.html)
|
||||
- [Importance of keys (Medium)](https://kentcdodds.com/blog/understanding-reacts-key-prop)
|
||||
|
||||
### Complete Reference
|
||||
```rust
|
||||
let text = "example";
|
||||
|
||||
cx.render(rsx!{
|
||||
div {
|
||||
h1 { "Example" },
|
||||
|
||||
{title}
|
||||
|
||||
// fstring interpolation
|
||||
"{text}"
|
||||
|
||||
p {
|
||||
// Attributes
|
||||
tag: "type",
|
||||
|
||||
// Anything that implements display can be an attribute
|
||||
abc: 123,
|
||||
|
||||
enabled: true,
|
||||
|
||||
// attributes also supports interpolation
|
||||
// `class` is not a restricted keyword unlike JS and ClassName
|
||||
class: "big small wide short {text}",
|
||||
|
||||
class: format_args!("attributes take fmt::Arguments. {}", 99),
|
||||
|
||||
tag: {"these tokens are placed directly"}
|
||||
|
||||
// Children
|
||||
a { "abcder" },
|
||||
|
||||
// Children with attributes
|
||||
h2 { "hello", class: "abc-123" },
|
||||
|
||||
// Child components
|
||||
CustomComponent { a: 123, b: 456, key: "1" },
|
||||
|
||||
// Child components with paths
|
||||
crate::components::CustomComponent { a: 123, b: 456, key: "1" },
|
||||
|
||||
// Iterators
|
||||
{ (0..3).map(|i| rsx!( h1 {"{:i}"} )) },
|
||||
|
||||
// More rsx!, or even html!
|
||||
{ rsx! { div { } } },
|
||||
{ html! { <div> </div> } },
|
||||
|
||||
// Matching
|
||||
// Requires rendering the nodes first.
|
||||
// rsx! is lazy, and the underlying closures cannot have the same type
|
||||
// Rendering produces the VNode type
|
||||
{match rand::gen_range::<i32>(1..3) {
|
||||
1 => rsx!(cx, h1 { "big" })
|
||||
2 => rsx!(cx, h2 { "medium" })
|
||||
_ => rsx!(cx, h3 { "small" })
|
||||
}}
|
||||
|
||||
// Optionals
|
||||
{true.and_then(|f| rsx!( h1 {"Conditional Rendering"} ))}
|
||||
|
||||
// Child nodes
|
||||
{cx.props.children}
|
||||
|
||||
// Any expression that is `IntoVNode`
|
||||
{expr}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
## Testing
|
||||
|
||||
To test your Rust code, you can annotate any function with the `#[test]` block. In VSCode with RA, this will provide a lens to click and run the test.
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn component_runs() {
|
||||
assert!(true)
|
||||
}
|
||||
```
|
||||
|
||||
This will test your Rust code _without_ going through the browser. This is ideal for squashing logic bugs and ensuring components render appropriately when the browsers's DOM is not needed. If you need to run tests in the browser, you can annotate your blocks with the `#[dioxus::test]` block.
|
||||
|
||||
```rust
|
||||
#[dioxus::test]
|
||||
fn runs_in_browser() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Then, when you run
|
||||
|
||||
```console
|
||||
$ dioxus test --chrome
|
||||
```
|
||||
|
||||
Dioxus will build and test your code using the Chrome browser as a harness.
|
||||
|
||||
There's a lot more to testing if you dive in, so check out the [Testing]() guide for more information
|
||||
Reference in new issue
Block a user