Refactor: move Component docs into the components directory to reflect how they are presented in the guide
This commit is contained in:
1 parent
67766c7b59
commit
02d8e66f40
7 files changed
+6
-6
No files matched your search
@@ -1,219 +0,0 @@
|
||||
# Passing children and attributes
|
||||
|
||||
Often times, you'll want to wrap some important functionality *around* your state, not directly nested *inside* another component. In these cases, you'll want to pass elements and attributes into a component and let the component place them appropriately.
|
||||
|
||||
In this chapter, you'll learn about:
|
||||
- Passing elements into components
|
||||
- Passing attributes into components
|
||||
|
||||
|
||||
## The use case
|
||||
|
||||
Let's say you're building a user interface and want to make some part of it a clickable link to another website. You would normally start with the HTML `<a>` tag, like so:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
a {
|
||||
href: "https://google.com"
|
||||
"Link to google"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
But, what if we wanted to style our `<a>` tag? Or wrap it with some helper icon? We could abstract our RSX into its own component:
|
||||
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
title: &'a str
|
||||
}
|
||||
|
||||
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}"
|
||||
"{cx.props.title}"
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
And then use it in our code like so:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
title: "Link to Google"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Let's say we don't just want the text to be clickable, but we want another element, like an image, to be clickable. How do we implement that?
|
||||
|
||||
## Passing children
|
||||
|
||||
If we want to pass an image into our component, we can just adjust our props and component to allow any `Element`.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
body: Element<'a>
|
||||
}
|
||||
|
||||
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}",
|
||||
&cx.props.body
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Then, at the call site, we can render some nodes and pass them in:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
body: cx.render(rsx!(
|
||||
img { src: "https://www.google.com/logos/doodles/..." }
|
||||
))
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Auto Conversion of the `Children` field
|
||||
|
||||
This pattern can become tedious in some instances, so Dioxus actually performs an implicit conversion of any `rsx` calls inside components into `Elements` at the `children` field. This means you must explicitly declare if a component can take children.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
href: &'a str,
|
||||
children: Element<'a>
|
||||
}
|
||||
|
||||
fn Clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
href: "{cx.props.href}",
|
||||
&cx.props.children
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Now, whenever we use `Clickable` in another component, we don't need to call `render` on child nodes - it will happen automatically!
|
||||
```rust
|
||||
rsx!(
|
||||
Clickable {
|
||||
href: "https://google.com"
|
||||
img { src: "https://www.google.com/logos/doodles/...." }
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> Note: Passing children into components will break any memoization due to the associated lifetime.
|
||||
|
||||
While technically allowed, it's an antipattern to pass children more than once in a component and will probably cause your app to crash.
|
||||
|
||||
However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format:
|
||||
|
||||
```rust
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
match cx.props.children {
|
||||
Some(VNode::Text(text)) => {
|
||||
// ...
|
||||
}
|
||||
_ => {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- ## Passing attributes
|
||||
|
||||
In the cases where you need to pass arbitrary element properties into a component - say to add more functionality to the `<a>` tag, Dioxus will accept any quoted fields. This is similar to adding arbitrary fields to regular elements using quotes.
|
||||
|
||||
```rust
|
||||
|
||||
rsx!(
|
||||
Clickable {
|
||||
"class": "blue-button",
|
||||
"style": "background: red;"
|
||||
}
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
For a component to accept these attributes, you must add an `attributes` field to your component's properties. We can use the spread syntax to add these attributes to whatever nodes are in our component.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
attributes: Attributes<'a>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
..cx.props.attributes,
|
||||
"Any link, anywhere"
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
The quoted escapes are a great way to make your components more flexible.
|
||||
-->
|
||||
|
||||
## Passing handlers
|
||||
|
||||
Dioxus also provides some implicit conversions from listener attributes into an `EventHandler` for any field on components that starts with `on`. IE `onclick`, `onhover`, etc. For properties, we want to define our `on` fields as an event handler:
|
||||
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct ClickableProps<'a> {
|
||||
onclick: EventHandler<'a, MouseEvent>
|
||||
}
|
||||
|
||||
fn clickable(cx: Scope<ClickableProps>) -> Element {
|
||||
cx.render(rsx!(
|
||||
a {
|
||||
onclick: move |evt| cx.props.onclick.call(evt)
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Then, we can attach a listener at the call site:
|
||||
|
||||
```rust
|
||||
rsx!(
|
||||
Clickable {
|
||||
onclick: move |_| log::info!("Clicked"),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Currently, Dioxus does not support an arbitrary amount of listeners - they must be strongly typed in `Properties`. If you need this use case, you can pass in an element with these listeners, or dip down into the `NodeFactory` API.
|
||||
|
||||
|
||||
## Wrapping up
|
||||
|
||||
In this chapter, we learned:
|
||||
- How to pass arbitrary nodes through the tree
|
||||
- How the `children` field works on component properties
|
||||
- How the `attributes` field works on component properties
|
||||
- How to convert `listeners` into `EventHandlers` for components
|
||||
- How to extend any node with custom attributes and children
|
||||
|
||||
Next chapter, we'll talk about conditionally rendering parts of your user interface.
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
# Introduction to Components
|
||||
|
||||
In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components.
|
||||
|
||||
In this chapter, we'll learn:
|
||||
- What makes a Component
|
||||
- How to model a component and its properties in Dioxus
|
||||
- How to "think declaratively"
|
||||
|
||||
## What is a component?
|
||||
|
||||
In short, a component is a special function that takes input properties and outputs an Element. Typically, Components serve a single purpose: group functionality of a User Interface. Much like a function encapsulates some specific computation task, a Component encapsulates some specific rendering task.
|
||||
|
||||
### Learning through prior art
|
||||
|
||||
Let's take a look at a post on r/rust and see if we can sketch out a component representation.
|
||||
|
||||

|
||||
|
||||
This component has a bunch of important information:
|
||||
|
||||
- The score
|
||||
- The number of comments
|
||||
- How long ago it was posted
|
||||
- The url short address
|
||||
- The title
|
||||
- The username of the original poster
|
||||
|
||||
If we wanted to sketch out these requirements in Rust, we would start with a struct:
|
||||
|
||||
```rust
|
||||
struct PostData {
|
||||
score: i32,
|
||||
comment_count: u32,
|
||||
post_time: Instant,
|
||||
url: String,
|
||||
title: String,
|
||||
original_poster_name: String
|
||||
}
|
||||
```
|
||||
|
||||
If we look at the layout of the component, we notice quite a few buttons and pieces of functionality:
|
||||
|
||||
- Upvote/Downvote
|
||||
- View comments
|
||||
- Share
|
||||
- Save
|
||||
- Hide
|
||||
- Give award
|
||||
- Report
|
||||
- Crosspost
|
||||
- Filter by site
|
||||
- View article
|
||||
- Visit user
|
||||
|
||||
If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into some core pieces:
|
||||
|
||||

|
||||
|
||||
- **VoteButton**: Upvote/Downvote
|
||||
- **TitleCard**: Title, Filter-By-Url
|
||||
- **MetaCard**: Original Poster, Time Submitted
|
||||
- **ActionCard**: View comments, Share, Save, Hide, Give award, Report, Crosspost
|
||||
|
||||
### Modeling with Dioxus
|
||||
|
||||
We can start by sketching out the Element hierarchy using Dioxus. In general, our "Post" component will be comprised of the four sub-components listed above. First, let's define our `Post` component.
|
||||
|
||||
Unlike normal functions, Dioxus components must explicitly define a single struct to contain all the inputs. These are commonly called "Properties" (props). Our component will be a combination of these properties and a function to render them.
|
||||
|
||||
Our props must implement the `Props` trait and - if the component does not borrow any data - `PartialEq`. Both of these can be done automatically through derive macros:
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct PostProps {
|
||||
id: Uuid,
|
||||
score: i32,
|
||||
comment_count: u32,
|
||||
post_time: Instant,
|
||||
url: String,
|
||||
title: String,
|
||||
original_poster: String
|
||||
}
|
||||
```
|
||||
|
||||
And our render function:
|
||||
```rust
|
||||
fn Post(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
VoteButton {
|
||||
score: cx.props.score,
|
||||
}
|
||||
TitleCard {
|
||||
title: cx.props.title,
|
||||
url: cx.props.url,
|
||||
}
|
||||
MetaCard {
|
||||
original_poster: cx.props.original_poster,
|
||||
post_time: cx.props.post_time,
|
||||
}
|
||||
ActionCard {
|
||||
post_id: cx.props.id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Our `Post` component is simply a collection of smaller components wrapped together in a single container.
|
||||
|
||||
Let's take a look at the `VoteButton` component. For now, we won't include any interactivity - just the rendering the score and buttons to the screen.
|
||||
|
||||
Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`.
|
||||
|
||||
As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`.
|
||||
|
||||
```rust
|
||||
#[derive(PartialEq, Props)]
|
||||
struct VoteButtonProps {
|
||||
score: i32
|
||||
}
|
||||
|
||||
fn VoteButton(cx: Scope<VoteButtonProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "votebutton"
|
||||
div { class: "arrow up" }
|
||||
div { class: "score", "{cx.props.score}"}
|
||||
div { class: "arrow down" }
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Borrowing
|
||||
|
||||
You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc<str>`.
|
||||
|
||||
Because we're working in Rust, we can choose to either use `Rc<str>`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned.
|
||||
|
||||
To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct TitleCardProps<'a> {
|
||||
title: &'a str,
|
||||
}
|
||||
|
||||
fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow.
|
||||
|
||||
This means that during the render process, a newer version of `TitleCardProps` will never be compared with a previous version, saving some clock cycles.
|
||||
|
||||
## The inline_props macro
|
||||
|
||||
Yes - *another* macro! However, this one is entirely optional.
|
||||
|
||||
For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component.
|
||||
|
||||
Our title card above would be transformed from:
|
||||
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct TitleCardProps {
|
||||
title: String,
|
||||
}
|
||||
|
||||
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn TitleCard(cx: Scope, title: String) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality.
|
||||
|
||||
However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate.
|
||||
|
||||
## The `Scope` object
|
||||
|
||||
Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration.
|
||||
|
||||
Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state?
|
||||
|
||||
React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs.
|
||||
|
||||
```javascript
|
||||
function Component(props) {
|
||||
let [state, set_state] = useState(10);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Because Dioxus needs to work with the rules of Rust it uses the `Scope` object to maintain some internal bookkeeping. That's what the `Scope` object is: a place for the component to store state, manage listeners, and allocate elements. Advanced users of Dioxus will want to learn how to properly leverage the `Scope` object to build robust and performant extensions for Dioxus.
|
||||
|
||||
```rust
|
||||
fn Post(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!("hello"))
|
||||
}
|
||||
```
|
||||
## Moving forward
|
||||
|
||||
Next chapter, we'll talk about composing Elements and Components across files to build a larger Dioxus App.
|
||||
|
||||
For more references on components, make sure to check out:
|
||||
|
||||
- [Components in depth]()
|
||||
- [Lifecycles]()
|
||||
- [The Scope object]()
|
||||
- [Optional Prop fields]()
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
# Thinking in React
|
||||
|
||||
We've finally reached the point in our tutorial where we can talk about the "Theory of React." We've talked about defining a declarative view, but not about the aspects that make our code *reactive*.
|
||||
|
||||
Understanding the theory of reactive programming is essential to making sense of Dioxus and writing effective, performant UIs.
|
||||
|
||||
In this section, we'll talk about:
|
||||
|
||||
- One-way data flow
|
||||
- Modifying data
|
||||
- Forcing renders
|
||||
- How renders propagate
|
||||
|
||||
This section is a bit long, but worth the read. We recommend coffee, tea, and/or snacks.
|
||||
|
||||
## Reactive Programming
|
||||
|
||||
Dioxus is one the very few Rust libraries that provide a "Reactive Programming Model". The term "Reactive programming" is a classification of programming paradigm - much like functional or imperative programming. This is a very important distinction since it affects how we *think* about our code.
|
||||
|
||||
Reactive programming is a programming model concerned with deriving computations from asynchronous data flow. Most reactive programs are comprised of datasources, intermediate computations, and a final result.
|
||||
|
||||
We consider the rendered GUI to be the final result of our Dioxus apps. The datasources for our apps include local and global state.
|
||||
|
||||
For example, the model presented in the figure below is comprised of two data sources: time and a constant. These values are passed through our computation graph to achieve a final result: `g`.
|
||||
|
||||

|
||||
|
||||
Whenever our `seconds` variable changes, we will then reevaluate the computation for `t`. Because `g` relies on `t`, we will also reevaluate its computation too. Notice that we would've reevaluated the computation for `g` even if `t` didn't change because `seconds` is used to calculate `g`.
|
||||
|
||||
However, if we somehow changed our constant from `1` to `2`, then we need to reevaluate `t`. If, for whatever reason, this change did not affect the result of `t`, then we wouldn't try to reevaluate `g`.
|
||||
|
||||
In Reactive Programming, we don't think about whether or not we should reevaluate `t` or `g`; instead, we simply provide functions of computation and let the framework figure out the rest for us.
|
||||
|
||||
In Rust, our reactive app would look something like:
|
||||
|
||||
```rust
|
||||
fn compute_g(t: i32, seconds: i32) -> bool {
|
||||
t > seconds
|
||||
}
|
||||
|
||||
fn compute_t(constant: i32, seconds: i32) -> i32 {
|
||||
constant + seconds
|
||||
}
|
||||
|
||||
fn compute_graph(constant: i32, seconds: i32) -> bool {
|
||||
let t = compute_t(constant, seconds);
|
||||
let g = compute_g(t, seconds);
|
||||
g
|
||||
}
|
||||
```
|
||||
|
||||
## How is Dioxus Reactive?
|
||||
|
||||
The Dioxus VirtualDom provides us a framework for reactive programming. When we build apps with dioxus, we need to provide our own datasources. This can be either initial props or some values fetched from the network. We then pass this data through our app into components through properties.
|
||||
|
||||
If we represented the reactive graph presented above in Dioxus, it would look very similar:
|
||||
|
||||
```rust
|
||||
// Declare a component that holds our datasources and calculates `g`
|
||||
fn RenderGraph(cx: Scope) -> Element {
|
||||
let seconds = use_datasource(SECONDS);
|
||||
let constant = use_state(&cx, || 1);
|
||||
|
||||
cx.render(rsx!(
|
||||
RenderG { seconds: seconds }
|
||||
RenderT { seconds: seconds, constant: constant }
|
||||
))
|
||||
}
|
||||
|
||||
// "calculate" g by rendering `t` and `seconds`
|
||||
#[inline_props]
|
||||
fn RenderG(cx: Scope, seconds: i32) -> Element {
|
||||
cx.render(rsx!{ "There are {seconds} seconds remaining..." })
|
||||
}
|
||||
|
||||
// calculate and render `t` in its own component
|
||||
#[inline_props]
|
||||
fn RenderT(cx: Scope, seconds: i32, constant: i32) -> Element {
|
||||
let res = seconds + constant;
|
||||
cx.render(rsx!{ "{res}" })
|
||||
}
|
||||
```
|
||||
|
||||
With this app, we've defined three components. Our top-level component provides our datasources (the hooks), computation nodes (child components), and a final value (what's "rendered").
|
||||
|
||||
Now, whenever the `constant` changes, our `RenderT` component will be re-rendered. However, if `seconds` doesn't change, then we don't need to re-render `RenderG` because the input is the same. If `seconds` *does* change, then both RenderG and RenderT will be reevaluated.
|
||||
|
||||
Dioxus is "Reactive" because it provides this framework for us. All we need to do is write our own tiny units of computation and Dioxus figures out which components need to be reevaluated automatically.
|
||||
|
||||
These extra checks and algorithms add some overhead, which is why you see projects like [Sycamore](http://sycamore-rs.netlify.app) and [SolidJS](http://solidjs.com) eliminating them altogether. Dioxus is *really* fast, so we're willing to exchange the added overhead for improved developer experience.
|
||||
|
||||
## How do we update values in our dataflow graph?
|
||||
|
||||
Dioxus will automatically figure out how to regenerate parts of our app when datasources change. But how exactly can we update our data sources?
|
||||
|
||||
In Dioxus there are two datasources:
|
||||
|
||||
1. Local state in `use_hook` and all other hooks
|
||||
2. Global state through `provide_context`.
|
||||
|
||||
Technically, the root props of the VirtualDom are a third datasource, but since we cannot modify them, they are not worth talking about.
|
||||
|
||||
### Local State
|
||||
|
||||
For local state in hooks, Dioxus gives us the `use_hook` method which returns an `&mut T` without any requirements. This means raw hook values are not tracked by Dioxus. In fact, we could write a component that modifies a hook value directly:
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope) -> Element {
|
||||
let mut count = cx.use_hook(|_| 0);
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
onclick: move |_| *count += 1,
|
||||
"Count: {count}"
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
However, when this value is written to, the component does not know to be reevaluated. We must explicitly tell Dioxus that this component is "dirty" and needs to be re-rendered. This is done through the `cx.needs_update` method:
|
||||
|
||||
```rust
|
||||
button {
|
||||
onclick: move |_| {
|
||||
*count += 1;
|
||||
cx.needs_update();
|
||||
},
|
||||
"Count: {count}"
|
||||
}
|
||||
```
|
||||
|
||||
Now, whenever we click the button, the value will change and the component will be re-rendered.
|
||||
|
||||
> Re-rendering is when Dioxus calls your function component *again*. Component functions will be called over and over throughout their lifetime, so they should be mostly side-effect free.
|
||||
|
||||
### Understand this!
|
||||
|
||||
Your component functions will be called ("rendered" in our lingo) for as long as the component is present in the tree.
|
||||
|
||||
A single component will be called multiple times, modifying its own internal state or rendering new nodes with new values from its properties.
|
||||
|
||||
### App-Global State
|
||||
|
||||
With the `provide_context` and `consume_context` methods on `Scope`, we can share values to descendants without having to pass values through component props. This has the side-effect of making our datasources less obvious from a high-level perspective, but it makes our components more modular within the same codebase.
|
||||
|
||||
To make app-global state easier to reason about, Dioxus makes all values provided through `provide_context` immutable. This means any library built on top of `provide_context` needs to use interior mutability to modify shared global state.
|
||||
|
||||
In these cases, App-Global state needs to manually track which components need to be re-generated.
|
||||
|
||||
To regenerate *any* component in your app, you can get a handle to the Dioxus' internal scheduler through `schedule_update_any`:
|
||||
|
||||
```rust
|
||||
let force_render = cx.schedule_update_any();
|
||||
|
||||
// force a render of the root component
|
||||
force_render(ScopeId(0));
|
||||
```
|
||||
|
||||
## What does it mean for a component to "re-render"?
|
||||
|
||||
In our guides, we frequently use the phrase "re-render" to describe updates to our app. You'll often hear this paired with "preventing unnecessary re-renders." But what exactly does this mean?
|
||||
|
||||
When we call `dioxus::desktop::launch`, Dioxus will create a new `Scope` object and call the component we gave it. Our `rsx!` calls will create new nodes which we return back to the VirtualDom. Dioxus will then look through these nodes for child components, call their functions, and so on until every component has been "rendered." We consider these nodes "rendered" because they were created because of our explicit actions.
|
||||
|
||||
The tree of UI that dioxus creates will roughly look like the tree of components presented earlier:
|
||||
|
||||

|
||||
|
||||
But what happens when we call `needs_update` after modifying some important state? Well, if Dioxus called our component's function again, then we would produce new, different nodes. In fact, this is exactly what Dioxus does!
|
||||
|
||||
At this point, we have some old nodes and some new nodes. Again, we call this "rendering" because Dioxus had to create new nodes because of our explicit actions. Any time new nodes get created, our VirtualDom is being "rendered."
|
||||
|
||||
These nodes are stored in an extremely efficient memory allocator called a "bump arena." For example, a div with a handler and attribute would be stored in memory in two locations: the "old" tree and the "new" tree.
|
||||
|
||||

|
||||
|
||||
From here, Dioxus computes the difference between these trees and updates the Real DOM to make it look like the new version of what we've declared.
|
||||
|
||||

|
||||
|
||||
## Suppressing Renders
|
||||
|
||||
So, we know how to make Dioxus render, but how do we *stop* it? What if we *know* that our state didn't change and we shouldn't render and diff new nodes because they'll be exactly the same as the last time?
|
||||
|
||||
In these cases, you want to reach for *memoization*. In Dioxus, memoization involves preventing a component from rendering again if its props didn't change since the last time it attempted to render.
|
||||
|
||||
Visually, you can tell that a component will only re-render if the new value is sufficiently different than the old one.
|
||||
|
||||
| props.val | re-render |
|
||||
| --------- | --------- |
|
||||
| 10 | true |
|
||||
| 20 | true |
|
||||
| 20 | false |
|
||||
| 20 | false |
|
||||
| 10 | true |
|
||||
| 30 | false |
|
||||
|
||||
This is why when you `derive(Props)`, you must also implement the `PartialEq` trait. To override the memoization strategy for a component, you can simply implement your own PartialEq.
|
||||
|
||||
```rust
|
||||
struct CustomProps {
|
||||
val: i32,
|
||||
}
|
||||
|
||||
impl PartialEq for CustomProps {
|
||||
fn partial_eq(&self, other: &Self) -> bool {
|
||||
// we don't render components that have a val less than 5
|
||||
if other.val > 5 && self.val > 5{
|
||||
self.val == other.val
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
However, for components that borrow data, it doesn't make sense to implement PartialEq since the actual references in memory might be different.
|
||||
|
||||
You can technically override this behavior by implementing the `Props` trait manually, though it's unsafe and easy to mess up:
|
||||
|
||||
```rust
|
||||
impl Properties for CustomProps {
|
||||
fn memoize(&self, other &Self) -> bool {
|
||||
self != other
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
TLDR:
|
||||
- Dioxus checks if props changed between renders
|
||||
- If props changed according to PartialEq, Dioxus re-renders the component
|
||||
- Props that have a lifetime (ie `<'a>`) will always be re-rendered
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Wow, that was a lot of material!
|
||||
|
||||
Let's see if we can recap what was presented:
|
||||
|
||||
- Reactive programming calculates a final value from datasources and computation
|
||||
- Dioxus is "reactive" since it figures out which computations to check
|
||||
- `schedule_update` must be called to mark a component as dirty
|
||||
- dirty components will be re-rendered (called multiple times) to produce a new UI
|
||||
- Renders can be suppressed with memoization
|
||||
|
||||
This theory is crucial to understand how to compose components and how to control renders in your app.
|
||||
@@ -1,302 +0,0 @@
|
||||
|
||||
# Reusing, Importing, and Exporting Components
|
||||
|
||||
As your application grows in size, you'll want to start breaking your UI into components and, eventually, different files. This is a great idea to encapsulate functionality of your UI and scale your team.
|
||||
|
||||
In this chapter we'll cover:
|
||||
- Rust's modules
|
||||
- Pub/Private components
|
||||
- Structure for large components
|
||||
|
||||
## Breaking it down
|
||||
Let's say our app looks something like this:
|
||||
|
||||
```shell
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
fn App(Scope) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct PostProps{}
|
||||
fn Post(Scope<PostProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct VoteButtonsProps {}
|
||||
fn VoteButtons(Scope<VoteButtonsProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct TitleCardProps {}
|
||||
fn TitleCard(Scope<TitleCardProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct MetaCardProps {}
|
||||
fn MetaCard(Scope<MetaCardProps>) -> Element {}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ActionCardProps {}
|
||||
fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
That's a lot of components for one file! We've successfully refactored our app into components, but we should probably start breaking it up into a file for each component.
|
||||
|
||||
## Breaking into different files
|
||||
|
||||
Fortunately, Rust has a built-in module system that's much cleaner than what you might be used to in JavaScript. Because `VoteButtons`, `TitleCard`, `MetaCard`, and `ActionCard` all belong to the `Post` component, let's put them all in a folder together called "post". We'll make a file for each component and move the props and render function.
|
||||
|
||||
```rust
|
||||
// src/post/action.rs
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct ActionCardProps {}
|
||||
fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file.
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct PostProps {}
|
||||
fn Post(Scope<PostProps>) -> Element {}
|
||||
```
|
||||
|
||||
```shell
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
├── main.rs
|
||||
└── post
|
||||
├── vote.rs
|
||||
├── title.rs
|
||||
├── meta.rs
|
||||
├── action.rs
|
||||
└── mod.rs
|
||||
```
|
||||
|
||||
In our `main.rs`, we'll want to declare the `post` module so we can access our `Post` component.
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
mod post;
|
||||
|
||||
fn App(Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
post::Post {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
comment_count: 10,
|
||||
post_time: std::Instant::now(),
|
||||
url: "example".to_string(),
|
||||
title: "Title".to_string(),
|
||||
original_poster: "me".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
If you tried to build this app right now, you'll get an error message saying that `Post is private, try changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public":
|
||||
|
||||
```rust
|
||||
// src/post/mod.rs
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct PostProps {}
|
||||
pub fn Post(Scope<PostProps>) -> Element {}
|
||||
```
|
||||
|
||||
While we're here, we also need to make sure each of our subcomponents are included as modules and exported.
|
||||
|
||||
Our "post/mod.rs" file will eventually look like this:
|
||||
|
||||
```rust
|
||||
use dioxus::prelude::*;
|
||||
|
||||
mod vote;
|
||||
mod title;
|
||||
mod meta;
|
||||
mod action;
|
||||
|
||||
#[derive(Props, PartialEq)]
|
||||
pub struct PostProps {
|
||||
id: uuid::Uuid,
|
||||
score: i32,
|
||||
comment_count: u32,
|
||||
post_time: std::time::Instant,
|
||||
url: String,
|
||||
title: String,
|
||||
original_poster: String
|
||||
}
|
||||
|
||||
pub fn Post(Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
vote::VoteButtons {
|
||||
score: props.score,
|
||||
}
|
||||
title::TitleCard {
|
||||
title: props.title,
|
||||
url: props.url,
|
||||
}
|
||||
meta::MetaCard {
|
||||
original_poster: props.original_poster,
|
||||
post_time: props.post_time,
|
||||
}
|
||||
action::ActionCard {
|
||||
post_id: props.id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Ultimately, including and exporting components is governed by Rust's module system. [The Rust book is a great resource to learn about these concepts in greater detail.](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html)
|
||||
|
||||
## Final structure:
|
||||
|
||||
```shell
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
├── main.rs
|
||||
└── post
|
||||
├── vote.rs
|
||||
├── title.rs
|
||||
├── meta.rs
|
||||
├── action.rs
|
||||
└── mod.rs
|
||||
```
|
||||
|
||||
```rust
|
||||
// main.rs:
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(App);
|
||||
}
|
||||
|
||||
mod post;
|
||||
|
||||
fn App(Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
post::Post {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
comment_count: 10,
|
||||
post_time: std::Instant::now(),
|
||||
url: "example".to_string(),
|
||||
title: "Title".to_string(),
|
||||
original_poster: "me".to_string()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```rust
|
||||
// src/post/mod.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
mod vote;
|
||||
mod title;
|
||||
mod meta;
|
||||
mod action;
|
||||
|
||||
#[derive(Props, PartialEq)]
|
||||
pub struct PostProps {
|
||||
id: uuid::Uuid,
|
||||
score: i32,
|
||||
comment_count: u32,
|
||||
post_time: std::time::Instant,
|
||||
url: String,
|
||||
title: String,
|
||||
original_poster: String
|
||||
}
|
||||
|
||||
pub fn Post(Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
div { class: "post-container"
|
||||
vote::VoteButtons {
|
||||
score: props.score,
|
||||
}
|
||||
title::TitleCard {
|
||||
title: props.title,
|
||||
url: props.url,
|
||||
}
|
||||
meta::MetaCard {
|
||||
original_poster: props.original_poster,
|
||||
post_time: props.post_time,
|
||||
}
|
||||
action::ActionCard {
|
||||
post_id: props.id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/post/vote.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct VoteButtonsProps {}
|
||||
pub fn VoteButtons(Scope<VoteButtonsProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/post/title.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct TitleCardProps {}
|
||||
pub fn TitleCard(Scope<TitleCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/post/meta.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct MetaCardProps {}
|
||||
pub fn MetaCard(Scope<MetaCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/post/action.rs
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
pub struct ActionCardProps {}
|
||||
pub fn ActionCard(Scope<ActionCardProps>) -> Element {}
|
||||
```
|
||||
|
||||
## Moving forward
|
||||
|
||||
Next chapter, we'll start to add use code to hide and show Elements with conditional rendering.
|
||||
|
||||
For more reading on components:
|
||||
|
||||
- [Components in depth]()
|
||||
- [Lifecycles]()
|
||||
- [The Context object]()
|
||||
- [Optional Prop fields]()
|
||||
@@ -130,7 +130,7 @@ Remember: keys let Dioxus uniquely identify an item among its siblings. A well-c
|
||||
|
||||
You might be tempted to use an item's index in the array as its key. In fact, that’s what Dioxus will use if you don’t specify a key at all. This is only acceptable if you can guarantee that the list is constant – i.e., no re-ordering, additions or deletions. In all other cases, do not use the index for the key – it will lead to the performance problems described above.
|
||||
|
||||
Note that if you pass the key to a [custom component](./components.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop:
|
||||
Note that if you pass the key to a [custom component](../components/index.md) you've made, it won't receive the key as a prop. It’s only used as a hint by Dioxus itself. If your component needs an ID, you have to pass it as a separate prop:
|
||||
```rust
|
||||
Post { key: "{key}", id: "{key}" }
|
||||
```
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# Component Properties
|
||||
|
||||
All component `properties` must implement the `Properties` trait. The `Props` macro automatically derives this trait but adds some additional functionality. In this section, we'll learn about:
|
||||
|
||||
- Using the props macro
|
||||
- Memoization through PartialEq
|
||||
- Optional fields on props
|
||||
- The inline_props macro
|
||||
|
||||
|
||||
|
||||
## Using the Props Macro
|
||||
|
||||
All `properties` that your components take must implement the `Properties` trait. The simplest props you can use is simply `()` - or no value at all. `Scope` is generic over your component's props and actually defaults to `()`.
|
||||
|
||||
```rust
|
||||
// this scope
|
||||
Scope<()>
|
||||
|
||||
// is the same as this scope
|
||||
Scope
|
||||
```
|
||||
|
||||
If we wanted to define a component with its own props, we would create a new struct and tack on the `Props` derive macro:
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct MyProps {
|
||||
name: String
|
||||
}
|
||||
```
|
||||
This particular code will not compile - all `Props` must either a) borrow from their parent or b) implement `PartialEq`. Since our props do not borrow from their parent, they are `'static` and must implement PartialEq.
|
||||
|
||||
For an owned example:
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct MyProps {
|
||||
name: String
|
||||
}
|
||||
```
|
||||
|
||||
For a borrowed example:
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct MyProps<'a> {
|
||||
name: &'a str
|
||||
}
|
||||
```
|
||||
|
||||
Then, to use these props in our component, we simply swap out the generic parameter on scope.
|
||||
|
||||
For owned props, we just drop it in:
|
||||
|
||||
```rust
|
||||
fn Demo(cx: Scope<MyProps>) -> Element {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
However, for props that borrow data, we need to explicitly declare lifetimes. Rust does not know that our props and our component share the same lifetime, so must explicitly attach a lifetime in two places:
|
||||
|
||||
```rust
|
||||
fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
By putting the `'a` lifetime on Scope and our Props, we can now borrow data from our parent and pass it on to our children.
|
||||
|
||||
|
||||
## Memoization
|
||||
|
||||
If you're coming from React, you might be wondering how memoization fits in. For our purpose, memoization is the process in which we check if a component actually needs to be re-rendered when its props change. If a component's properties change but they wouldn't necessarily affect the output, then we don't need to actually re-render the component.
|
||||
|
||||
For example, let's say we have a component that has two children:
|
||||
|
||||
```rust
|
||||
fn Demo(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || String::from("bob"));
|
||||
let age = use_state(&cx, || 21);
|
||||
|
||||
cx.render(rsx!{
|
||||
Name { name: name }
|
||||
Age { age: age }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
If `name` changes but `age` does not, then there is no reason to re-render our `Age` component since the contents of its props did not meaningfully change.
|
||||
|
||||
|
||||
Dioxus implements memoization by default, which means you can always rely on props with `PartialEq` or no props at all to act as barriers in your app. This can be extremely useful when building larger apps where properties frequently change. By moving our state into a global state management solution, we can achieve precise, surgical re-renders, improving the performance of our app.
|
||||
|
||||
|
||||
However, for components that borrow values from their parents, we cannot safely memoize them.
|
||||
|
||||
For example, this component borrows `&str` - and if the parent re-renders, then the actual reference to `str` will probably be different. Since the data is borrowed, we need to pass a new version down the tree.
|
||||
|
||||
```rust
|
||||
#[derive(Props)]
|
||||
struct MyProps<'a> {
|
||||
name: &'a str
|
||||
}
|
||||
|
||||
fn Demo<'a>(cx: Scope<'a, MyProps<'a>>) -> Element {
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
TLDR:
|
||||
- if you see props with a lifetime or generics, it cannot be memoized
|
||||
- memoization is done automatically through the `PartialEq` trait
|
||||
- components with empty props can act as memoization barriers
|
||||
|
||||
## Optional Fields
|
||||
|
||||
Dioxus' `Props` macro is very similar to [@idanarye](https://github.com/idanarye)'s [TypedBuilder crate](https://github.com/idanarye/rust-typed-builder) and supports many of the same parameters.
|
||||
|
||||
For example, you can easily create optional fields by attaching the `optional` modifier to a field.
|
||||
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct MyProps {
|
||||
name: String,
|
||||
|
||||
#[props(optional)]
|
||||
description: Option<String>
|
||||
}
|
||||
|
||||
fn Demo(cx: MyProps) -> Element {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Then, we can completely omit the description field when calling the component:
|
||||
|
||||
```rust
|
||||
rsx!{
|
||||
Demo {
|
||||
name: "Thing".to_string(),
|
||||
// description is omitted
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes:
|
||||
|
||||
- `default` - automatically add the field using its `Default` implementation
|
||||
- `strip_option` - automatically wrap values at the call site in `Some`
|
||||
- `optional` - combine both `default` and `strip_option`
|
||||
- `into` - automatically call `into` on the value at the callsite
|
||||
|
||||
For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new.
|
||||
|
||||
|
||||
|
||||
|
||||
## The inline_props macro
|
||||
|
||||
Yes - *another* macro! However, this one is entirely optional.
|
||||
|
||||
For internal components, we provide the `inline_props` macro, which will let you embed your `Props` definition right into the function arguments of your component.
|
||||
|
||||
Our title card above would be transformed from:
|
||||
|
||||
```rust
|
||||
#[derive(Props, PartialEq)]
|
||||
struct TitleCardProps {
|
||||
title: String,
|
||||
}
|
||||
|
||||
fn TitleCard(cx: Scope<TitleCardProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{cx.props.title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn TitleCard(cx: Scope, title: String) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality.
|
||||
|
||||
However, it's great for quickly throwing together an app without dealing with *any* extra boilerplate.
|
||||
Reference in new issue
Block a user