docs: strong improvmenets, first major section done

This commit is contained in:
Jonathan Kelley 2021-10-29 00:28:23 -04:00
parent b9b98dc7a5
commit 54d050cf89
11 changed files with 424 additions and 31 deletions

View File

@ -1,2 +0,0 @@

View File

@ -1,13 +1,12 @@
# Core Topics
In this chapter of the book, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app.
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.

View File

@ -2,9 +2,14 @@
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 an input and outputs a group of Elements. 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.
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
@ -24,7 +29,7 @@ This component has a bunch of important information:
If we wanted to sketch out these requirements in Rust, we would start with a struct:
```rust
struct Post {
struct PostData {
score: i32,
comment_count: u32,
post_time: Instant,
@ -52,20 +57,20 @@ If we included all this functionality in one `rsx!` call, it would be huge! Inst
![Post as Component](../images/reddit_post_components.png)
- **VoteButtons**: Upvote/Downvote
- **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
When designing these components, we can start by sketching out the hierarchy using Dioxus. In general, our "Post" component will simply be comprised of our four sub-components. We would start the process by defining our "Post" component. Our component will take in all of the important data we listed above as part of its input.
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 `Properties` trait and - if the component does not borrow any data - `PartialEq`. Both of these can be done automatically through derive macros:
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(Properties, PartialEq)]
#[derive(Props, PartialEq)]
struct PostProps {
id: Uuid,
score: i32,
@ -82,7 +87,7 @@ And our render function:
fn Post((cx, props): Component<PostProps>) -> Element {
cx.render(rsx!{
div { class: "post-container"
VoteButtons {
VoteButton {
score: props.score,
}
TitleCard {
@ -101,21 +106,23 @@ fn Post((cx, props): Component<PostProps>) -> Element {
}
```
When we render components, we use the traditional Rust struct syntax to declare their properties. Dioxus will automatically call "into" on the property fields, cloning when necessary. Notice how our `Post` component is simply a collection of important smaller components wrapped together in a single container.
When declaring a component in `rsx!`, we can pass in properties using the traditional Rust struct syntax. Dioxus will automatically call "into" on the property fields, cloning when necessary. Our `Post` component is simply a collection of smaller components wrapped together in a single container.
Let's take a look at the `VoteButtons` component. For now, we won't include any interactivity - just the rendering the vote buttons and score to the screen.
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 tuple of `Context` and `&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`.
Most of your Components will look exactly like this: a Props struct and a render function. 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`. Every component must take a tuple of `Context` and `&Props` and return an `Element`.
```rust
#[derive(PartialEq, Props)]
struct VoteButtonsProps {
struct VoteButtonProps {
score: i32
}
fn VoteButtons((cx, props): Component<VoteButtonsProps>) -> Element {
fn VoteButton((cx, props): Component<VoteButtonProps>) -> Element {
cx.render(rsx!{
div { class: "votebuttons"
div { class: "votebutton"
div { class: "arrow up" }
div { class: "score", "{props.score}"}
div { class: "arrow down" }
@ -126,14 +133,14 @@ fn VoteButtons((cx, props): Component<VoteButtonsProps>) -> Element {
## Borrowing
You can avoid clones 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 simply 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>`.
You can avoid clones 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(Properties)]
#[derive(Props)]
struct TitleCardProps<'a> {
title: &'a str,
}
@ -145,29 +152,32 @@ fn TitleCard<'a>((cx, props): Component<'a, TitleCardProps>) -> Element<'a> {
}
```
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 `Context` object
Though very similar with React, Dioxus is different in a few ways. Most notably, React components will not have a `Context` parameter in the component declaration.
Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Context` 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 which must be carefully managed, especially in environments with multiple React roots - like the server.
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({}) {
function Component(props) {
let [state, set_state] = useState(10);
}
```
Because Dioxus needs to work with the rules of Rust, we need to provide a way for the component to do some internal bookkeeping. That's what the `Context` 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 `Context` object to build robust, performant extensions for Dioxus.
Because Dioxus needs to work with the rules of Rust it uses the `Context` object to maintain some internal bookkeeping. That's what the `Context` 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 `Context` object to build robust and performant extensions for Dioxus.
```rust
fn Post((cx /* <-- our Context object*/, props): Component<PostProps>) -> Element {
cx.render(rsx!{ })
}
```
## Moving forward
Next chapter, we'll talk about composing Elements and Components across files to build a larger Dioxus App.

View File

@ -1 +1,175 @@
# Conditional Rendering
Your components will often need to display different things depending on different conditions. With Dioxus, we can use Rust's normal control flow to conditional hide, show, and modify the structure of our markup.
In this chapter, you'll learn:
- How to return different Elements depending on a condition
- How to conditionally include an Element in your structure
- Common patterns like matching and bool mapping
## Conditionally returning Elements
In some components, you might want to render different markup given some condition. The typical example for conditional rendering is showing a "Log In" screen for users who aren't logged into your app. To break down this condition, we can consider two states:
- Logged in: show the app
- Logged out: show the login screen
Using the knowledge from the previous section on components, we'll start by making the app's props:
```rust
#[derive(Props, PartialEq)]
struct AppProps {
logged_in: bool
}
```
Now that we have a "logged_in" flag accessible in our props, we can render two different screens:
```rust
fn App((cx, props): Component<AppProps>) -> Element {
if props.logged_in {
cx.render(rsx!{
DashboardScreen {}
})
} else {
cx.render(rsx!{
LoginScreen {}
})
}
}
```
When the user is logged in, then this component will return the DashboardScreen. Else, the component will render the LoginScreen.
## Using match statements
Rust provides us algebraic datatypes: enums that can contain values. Using the `match` keyword, we can execute different branches of code given a condition.
For instance, we could run a function that returns a Result:
```rust
fn App((cx, props): Component<()>) -> Element {
match get_name() {
Ok(name) => cx.render(rsx!( "Hello, {name}!" )),
Err(err) => cx.render(rsx!( "Sorry, I don't know your name, because an error occurred: {err}" )),
}
}
```
We can even match against values:
```rust
fn App((cx, props): Component<()>) -> Element {
match get_name() {
"jack" => cx.render(rsx!( "Hey Jack, how's Diane?" )),
"diane" => cx.render(rsx!( "Hey Diana, how's Jack?" )),
name => cx.render(rsx!( "Hello, {name}!" )),
}
}
```
Do note: the `rsx!` macro returns a `Closure`, an anonymous function that has a unique type. Because every closure is unique, we cannot return *just* `rsx!` from a match statement. Every `rsx!` call must be rendered first when used in match statements.
To make patterns like these less verbose, the `rsx!` macro accepts an optional first argument on which it will call `render`. Our previous component can be shortened with this alternative syntax:
```rust
fn App((cx, props): Component<()>) -> Element {
match get_name() {
"jack" => rsx!(cx, "Hey Jack, how's Diane?" ),
"diane" => rsx!(cx, "Hey Diana, how's Jack?" ),
name => rsx!(cx, "Hello, {name}!" ),
}
}
```
This syntax even enables us to write a one-line component:
```rust
static App: Fc<()> = |(cx, props)| rsx!(cx, "hello world!");
```
## Nesting RSX
By looking at other examples, you might have noticed that it's possible to include `rsx!` calls inside other `rsx!` calls. With the curly-brace syntax, we can include anything in our `rsx!` that implements `IntoVnodeList`: a marker trait for iterators that produce Elements. `rsx!` itself implements this trait, so we can include it directly:
```rust
rsx!(
div {
{rsx!(
"more rsx!"
)}
}
)
```
As you might expect, we can refactor this structure into two separate calls using variables:
```rust
let title = rsx!( "more rsx!" );
rsx!(
div {
{title}
}
)
```
In the case of a log-in screen, we might want to display the same NavBar and Footer for both logged in and logged out users. We can model this entirely by assigning a `screen` variable to a different Element depending on a condition:
```rust
let screen = match logged_in {
true => rsx!(cx, DashboardScreen {}),
false => rsx!(cx, LoginScreen {})
};
cx.render(rsx!{
Navbar {}
{screen}
Footer {}
})
```
## Boolean Mapping
In the spirit of highly-functional apps, we suggest using the "boolean mapping" pattern when trying to conditionally hide/show an Element.
By default, Rust lets you convert any `boolean` into any other type by calling `and_then()`. We can exploit this functionality in components by mapping to some Element.
```rust
let show_title = true;
rsx!(
div {
{show_title.and_then(|| rsx!{
"This is the title"
})}
}
)
```
We can use this pattern for many things, including options:
```rust
let user_name = Some("bob");
rsx!(
div {
{user_name.map(|name| rsx!("Hello {name}"))}
}
)
```
## Rendering Nothing
Sometimes, you don't want your component to return anything at all. In these cases, we can pass "None" into our bracket. However, Rust is not able to infer that our `None` corresponds to `Element` so we need to cast it appropriately:
```rust
rsx!(cx, {None as Element} )
```
## Moving Forward:
In this chapter, we learned how to render different Elements from a Component depending on a condition. This is a very powerful building block to assemble complex User Interfaces!
In the next chapter, we'll cover how to renderer lists inside your `rsx!`.
Related Reading:
- [RSX in Depth]()

View File

@ -1 +1,11 @@
# Error handling
Astute observers might have noticed that `Element` is actually a type alias for `Option<VNode>`. You don't need to know what a `VNode` is, but it's important to recognize that we could actually return nothing at all:
```rust
fn App((cx, props): Component<()>) -> Element {
None
}
```

View File

@ -3,6 +3,12 @@
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
@ -125,7 +131,7 @@ mod title;
mod meta;
mod action;
#[derive(Properties, PartialEq)]
#[derive(Props, PartialEq)]
pub struct PostProps {
id: uuid::Uuid,
score: i32,
@ -210,7 +216,7 @@ mod title;
mod meta;
mod action;
#[derive(Properties, PartialEq)]
#[derive(Props, PartialEq)]
pub struct PostProps {
id: uuid::Uuid,
score: i32,

View File

@ -1 +1,167 @@
# Conditional Lists and Keys
You will often want to display multiple similar components from a collection of data.
In this chapter, you will learn:
- How to use iterators in `rsx!`
- How to filter and transform data into a list of Elements
- How to create efficient lists with keys
## Rendering data from lists
Thinking back to our analysis of the `r/reddit` page, we notice a list of data that needs to be rendered: the list of posts. This list of posts is always changing, so we cannot just hardcode the lists into our app like:
```rust
rsx!(
div {
Post {/* some properties */}
Post {/* some properties */}
Post {/* some properties */}
}
)
```
Instead, we need to transform the list of data into a list of Elements.
For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`.
As a simple example, let's render a list of names. First, start with our input data:
```rust
let names = ["jim", "bob", "jane", "doe"];
```
Then, we create a new iterator by calling `iter` and then `map`. In our `map` function, we'll place render our template.
```rust
let name_list = names.iter().map(|name| rsx!(
li { "{name}" }
));
```
Finally, we can include this list in the final structure:
```rust
rsx!(
ul {
{name_list}
}
)
```
The HTML-rendered version of this list would follow what you would expect:
```html
<ul>
<li> jim </li>
<li> bob </li>
<li> jane </li>
<li> doe </li>
</ul>
```
### Rendering our posts with a PostList component
Let's start by modeling this problem with a component and some properties.
For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List.
```rust
#[derive(Props, PartialEq)]
struct PostListProps<'a> {
posts: &'a [PostData]
}
```
Next, we're going to define our component:
```rust
fn App((cx, props): Component<PostList>) -> Element {
// First, we create a new iterator by mapping the post array
let posts = props.posts.iter().map(|post| rsx!{
Post {
title: post.title,
age: post.age,
original_poster: post.original_poster
}
});
// Finally, we render the post list inside of a container
cx.render(rsx!{
ul { class: "post-list"
{posts}
}
})
}
```
## Filtering Iterators
Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check.
As a very simple example, let's set up a filter where we only list names that begin with the letter "J".
Let's make our list of names:
```rust
let names = ["jim", "bob", "jane", "doe"];
```
Then, we create a new iterator by calling `iter`, then `filter`, then `map`. In our `filter` function, we'll only allow "j" names, and in our `map` function, we'll render our template.
```rust
let name_list = names
.iter()
.filter(|name| name.starts_with('j'))
.map(|name| rsx!( li { "{name}" }));
```
Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce.
For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call.
The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us.
## Keeping list items in order with key
The examples above demonstrate the power of iterators in `rsx!` but all share the same issue: a lack of "keys". Whenever you render a list of elements, each item in the list must be **uniquely identifiable**. To make each item unique, you need to give it a "key".
In Dioxus, keys are strings that uniquely identifies it among other items in that array:
```rust
rsx!( li { key: "a" } )
```
Keys tell Dioxus which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps Dioxus infer what exactly has happened, and make the correct updates to the screen
nb: the language from this section is strongly borrowed from [React's guide on keys](https://reactjs.org/docs/lists-and-keys.html).
### Where to get your key
Different sources of data provide different sources of keys:
- _Data from a database_: If your data is coming from a database, you can use the database keys/IDs, which are unique by nature.
- _Locally generated data_: If your data is generated and persisted locally (e.g. notes in a note-taking app), use an incrementing counter or a package like `uuid` when creating items.
### Rules of keys
- Keys must be unique among siblings. However, its okay to use the same keys for Elements in different arrays.
- Keys must not change or that defeats their purpose! Dont generate them while rendering.
### Why does Dioxus need keys?
Imagine that files on your desktop didnt have names. Instead, youd refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on.
File names in a folder and Element keys in an array serve a similar purpose. They let us uniquely identify an item between its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets Dioxus identify the item throughout its lifetime.
### Gotcha
You might be tempted to use an items index in the array as its key. In fact, thats what React will use if you dont specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs.
Similarly, do not generate keys on the fly, `gen_random`. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data.
Note that your components wont receive key as a prop. Its 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: "{id}" }
```

View File

@ -2,6 +2,12 @@
Every user interface you've ever used is just a symphony of tiny widgets working together to abstract over larger complex functions. In Dioxus, we call these tiny widgets "Elements." Using Components, you can easily compose Elements into larger groups to form even larger structures: Apps.
In this chapter, we'll cover:
- Declaring our first Element
- Composing Elements together
- Element properties
## Declaring our first Element
Because Dioxus is mostly used with HTML/CSS renderers, the default Element "collection" is HTML. Provided the `html` feature is not disabled, we can declare Elements using the `rsx!` macro:
```rust

View File

@ -1,8 +1,12 @@
# Hello World
Let's put together a simple "hello world" to get acquainted with Dioxus. The Dioxus-CLI has an equivalent to "create-react-app" built-in, but setting up Dioxus apps is simple enough to not need additional tooling.
Let's put together a simple "hello world" desktop application to get acquainted with Dioxus.
This demo will build a simple desktop app. Check out the platform-specific setup guides on how to port your app to different targets.
In this chapter, we'll cover:
- Starting a new Dioxus project with Cargo
- Adding Dioxus as a dependency
- Launching our first component as the app root
### A new project with Cargo

View File

@ -1,8 +1,14 @@
# Overview
Dioxus aims to provide a fast, friendly, and portable toolkit for building user interfaces with Rust.
In this chapter, we're going to get "set up" with a small desktop application.
This Getting Setup guide assumes you'll be building a small desktop application. You can check out the [Platform Specific Guides](../platforms/00-index.md) for more information on setting up Dioxus for any of the various supported platforms.
We'll learn about:
- Installing the Rust programming language
- Installing the Dioxus CLI for bundling and developing
- Suggested cargo extensions
For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md).
# Setting up Dioxus

View File

@ -17,4 +17,18 @@ fn html_usage() {
"hello world"
}
};
let items = ["bob", "bill", "jack"];
let f = items.iter().filter(|f| f.starts_with("b")).map(|f| {
rsx! {
"hello {f}"
}
});
let p = rsx! {
div {
{f}
}
};
}