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 @@
|
||||
# Event handlers
|
||||
@@ -0,0 +1,190 @@
|
||||
# Hooks and Internal State
|
||||
|
||||
In the [Adding Interactivity](./interactivity.md) section, we briefly covered the concept of hooks and state stored internal to components.
|
||||
|
||||
In this section, we'll dive a bit deeper into hooks, exploring both the theory and mechanics.
|
||||
|
||||
|
||||
|
||||
## Theory of Hooks
|
||||
|
||||
Over the past several decades, computer scientists and engineers have long sought the "right way" of designing user interfaces. With each new programming language, novel features are unlocked that change the paradigm in which user interfaces are coded.
|
||||
|
||||
Generally, a number of patterns have emerged, each with their own strengths and tradeoffs.
|
||||
|
||||
Broadly, there are two types of GUI structures:
|
||||
|
||||
- Immediate GUIs: re-render the entire screen on every update
|
||||
- Retained GUIs: only re-render the portion of the screen that changed
|
||||
|
||||
Typically, immediate-mode GUIs are simpler to write but can slow down as more features, like styling, are added.
|
||||
|
||||
Many GUIs today are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered. To help accommodate retained mode GUIs, like the web browser, Dioxus provides a mechanism to keep state around.
|
||||
|
||||
> Note: Even though hooks are accessible, you should still prefer to one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps.
|
||||
|
||||
## Mechanics of Hooks
|
||||
In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
We can even modify this value directly from an event handler:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button {
|
||||
onclick: move |_| name.push_str(".."),
|
||||
}
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Mechanically, each call to `use_hook` provides us with `&mut T` for a new value.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
let age: &mut u32 = cx.use_hook(|| 10);
|
||||
let friends: &mut Vec<String> = cx.use_hook(|| vec!["Jane Doe".to_string()]);
|
||||
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
Internally, Dioxus is creating a list of hook values with each call to `use_hook` advancing the index of the list to return the next value.
|
||||
|
||||
Our internal HookList would look something like:
|
||||
|
||||
```rust
|
||||
[
|
||||
Hook<String>,
|
||||
Hook<u32>,
|
||||
Hook<String>,
|
||||
]
|
||||
```
|
||||
|
||||
This is why hooks called out of order will fail - if we try to downcast a `Hook<String>` to `Hook<u32>`, Dioxus has no choice but to panic. We do provide a `try_use_hook` but you should never need that in practice.
|
||||
|
||||
This pattern might seem strange at first, but it can be a significant upgrade over structs as blobs of state, which tend to be difficult to use in [Rust given the ownership system](https://rust-lang.github.io/rfcs/2229-capture-disjoint-fields.html).
|
||||
|
||||
## Building new Hooks
|
||||
|
||||
However, most hooks you'll interact with *don't* return an `&mut T` since this is not very useful in a real-world situation.
|
||||
|
||||
Consider when we try to pass our `&mut String` into two different handlers:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &mut String = cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.push_str("yes"), }
|
||||
button { onclick: move |_| name.push_str("no"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Rust will not allow this to compile! We cannot `Copy` unique mutable references - they are, by definition, unique. However, we *can* reborrow our `&mut T` as an `&T` which are non-unique references and share those between handlers:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &String = &*cx.use_hook(|| "John Doe".to_string());
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| log::info!("{}", name), }
|
||||
button { onclick: move |_| log::info!("{}", name), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
So, for any custom hook we want to design, we need to enable mutation through [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) - IE move to runtime [borrow checking](https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html). We might incur a tiny runtime cost for each time we grab a new value from the hook, but this cost is extremely minimal.
|
||||
|
||||
This example uses the `Cell` type to let us replace the value through interior mutability. `Cell` has practically zero overhead, but is slightly more limited that its `RefCell` cousin.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name: &Cell<&'static str> = cx.use_hook(|| Cell::new("John Doe"));
|
||||
|
||||
cx.render(rsx!(
|
||||
button { onclick: move |_| name.set("John"), }
|
||||
button { onclick: move |_| name.set("Jane"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
## Driving state updates through hooks
|
||||
|
||||
Hooks like `use_state` and `use_ref` wrap this runtime borrow checking in a type that *does* implement `Copy`. Additionally, they also mark the component as "dirty" whenever a new value has been set. This way, whenever `use_state` has a new value `set`, the component knows to update.
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || "Jack");
|
||||
|
||||
cx.render(rsx!(
|
||||
"Hello, {name}"
|
||||
button { onclick: move |_| name.set("John"), }
|
||||
button { onclick: move |_| name.set("Jane"), }
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Internally, our `set` function looks something like this:
|
||||
|
||||
```rust
|
||||
impl<'a, T> UseState<'a, T> {
|
||||
fn set(&self, new: T) {
|
||||
// Replace the value in the cell
|
||||
self.value.set(new);
|
||||
|
||||
// Mark our component as dirty
|
||||
self.cx.needs_update();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Most hooks we provide implement `Deref` on their values since they are essentially smart pointers. To access the underlying value, you'll often need to use the deref operator:
|
||||
|
||||
```rust
|
||||
fn example(cx: Scope) -> Element {
|
||||
let name = use_state(&cx, || "Jack");
|
||||
|
||||
match *name {
|
||||
"Jack" => {}
|
||||
"Jill" => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// ..
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Hooks provided by the `Dioxus-Hooks` package
|
||||
|
||||
By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free to click on each hook to view its definition and associated documentation.
|
||||
|
||||
- [use_state](https://docs.rs/dioxus_hooks/use_state) - store state with ergonomic updates
|
||||
- [use_ref](https://docs.rs/dioxus_hooks/use_ref) - store non-clone state with a refcell
|
||||
- [use_future](https://docs.rs/dioxus_hooks/use_future) - store a future to be polled after initialization
|
||||
- [use_coroutine](https://docs.rs/dioxus_hooks/use_coroutine) - store a future that can be stopped/started/communicated with
|
||||
- [use_noderef](https://docs.rs/dioxus_hooks/use_noderef) - store a handle to the native element
|
||||
- [use_callback](https://docs.rs/dioxus_hooks/use_callback) - store a callback that implements PartialEq for memoization
|
||||
- [use_provide_context](https://docs.rs/dioxus_hooks/use_provide_context) - expose state to descendent components
|
||||
- [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context`
|
||||
|
||||
For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference.
|
||||
|
||||
## Wrapping up
|
||||
|
||||
In this chapter, we learned about the mechanics and intricacies of storing state inside a component.
|
||||
|
||||
In the next chapter, we'll cover event listeners in similar depth, and how to combine the two to build interactive components.
|
||||
@@ -0,0 +1,186 @@
|
||||
# Adding Interactivity
|
||||
|
||||
So far, we've learned how to describe the structure and properties of our user interfaces. Unfortunately, they're static and quite a bit uninteresting. In this chapter, we're going to learn how to add interactivity through events, state, and tasks.
|
||||
|
||||
## Primer on interactivity
|
||||
|
||||
Before we get too deep into the mechanics of interactivity, we should first understand how Dioxus exactly chooses to handle user interaction and updates to your app.
|
||||
|
||||
### What is state?
|
||||
|
||||
Every app you'll ever build has some sort of information that needs to be rendered to the screen. Dioxus is responsible for translating your desired user interface to what is rendered to the screen. *You* are responsible for providing the content.
|
||||
|
||||
The dynamic data in your user interface is called `State`.
|
||||
|
||||
When you first launch your app with `dioxus::web::launch_with_props` you'll be providing the initial state. You need to declare the initial state *before* starting the app.
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
// declare our initial state
|
||||
let props = PostProps {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
comment_count: 0,
|
||||
post_time: std::time::Instant::now(),
|
||||
url: String::from("dioxuslabs.com"),
|
||||
title: String::from("Hello, world"),
|
||||
original_poster: String::from("dioxus")
|
||||
};
|
||||
|
||||
// start the render loop
|
||||
dioxus::desktop::launch_with_props(Post, props);
|
||||
}
|
||||
```
|
||||
|
||||
When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope<PostProps>) -> Element {
|
||||
cx.render(rsx!{
|
||||
Title { title: &cx.props.title }
|
||||
Score { score: &cx.props.score }
|
||||
// etc
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
State in Dioxus follows a pattern called "one-way-data-flow." As your components create new components as their children, your app's structure will eventually grow into a tree where state gets passed down from the root component into "leaves" of the tree.
|
||||
|
||||
You've probably seen the tree of UI components represented using an directed-acyclic-graph:
|
||||
|
||||

|
||||
|
||||
With Dioxus, your state will always flow down from parent components into child components.
|
||||
|
||||
### How do I change my app's state?
|
||||
|
||||
We've talked about the data flow of state, but we haven't yet talked about how to change that state dynamically. Dioxus provides a variety of ways to change the state of your app while it's running.
|
||||
|
||||
For starters, we _could_ use the `update_root_props` method on the VirtualDom to provide an entirely new root state of your App. However, for most applications, you probably don't want to regenerate your entire app just to update some text or a flag.
|
||||
|
||||
Instead, you'll want to store state internally in your components and let *that* flow down the tree. To store state in your components, you'll use something called a `hook`. Hooks are special functions that reserve a slot of state in your component's memory and provide some functionality to update that state.
|
||||
|
||||
The most common hook you'll use for storing state is `use_state`. `use_state` provides a slot for some data that allows you to read and update the value without accidentally mutating it.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let post = use_state(&cx, || {
|
||||
PostData {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
comment_count: 0,
|
||||
post_time: std::time::Instant::now(),
|
||||
url: String::from("dioxuslabs.com"),
|
||||
title: String::from("Hello, world"),
|
||||
original_poster: String::from("dioxus")
|
||||
}
|
||||
});
|
||||
|
||||
cx.render(rsx!{
|
||||
Title { title: &post.title }
|
||||
Score { score: &post.score }
|
||||
// etc
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Whenever we have a new post that we want to render, we can call `set` on `post` and provide a new value:
|
||||
|
||||
```rust
|
||||
post.set(PostData {
|
||||
id: Uuid::new_v4(),
|
||||
score: 20,
|
||||
comment_count: 0,
|
||||
post_time: std::time::Instant::now(),
|
||||
url: String::from("google.com"),
|
||||
title: String::from("goodbye, world"),
|
||||
original_poster: String::from("google")
|
||||
})
|
||||
```
|
||||
|
||||
We'll dive deeper into how exactly these hooks work later.
|
||||
|
||||
### When do I update my state?
|
||||
|
||||
There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task.
|
||||
|
||||
### Updating state in listeners
|
||||
|
||||
When responding to user-triggered events, we'll want to "listen" for an event on some element in our component.
|
||||
|
||||
For example, let's say we provide a button to generate a new post. Whenever the user clicks the button, they get a new post. To achieve this functionality, we'll want to attach a function to the `on_click` method of `button`. Whenever the button is clicked, our function will run, and we'll get new Post data to work with.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let post = use_state(&cx, || PostData::new());
|
||||
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
on_click: move |_| post.set(PostData::random())
|
||||
"Generate a random post"
|
||||
}
|
||||
Post { props: &post }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
We'll dive much deeper into event listeners later.
|
||||
|
||||
### Updating state asynchronously
|
||||
|
||||
We can also update our state outside of event listeners with `coroutines`. `Coroutines` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component. Since coroutines stick around between renders, the data in them must be valid for the `'static` lifetime. We must explicitly declare which values our task will rely on to avoid the `stale props` problem common in React.
|
||||
|
||||
We can use tasks in our components to build a tiny stopwatch that ticks every second.
|
||||
|
||||
> Note: The `use_future` hook will start our coroutine immediately. The `use_coroutine` hook provides more flexibility over starting and stopping futures on the fly.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let mut sec_elapsed = use_state(&cx, || 0);
|
||||
|
||||
use_future(&cx, || {
|
||||
let mut sec_elapsed = sec_elapsed.for_async();
|
||||
async move {
|
||||
loop {
|
||||
TimeoutFuture::from_ms(1000).await;
|
||||
sec_elapsed += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx!(cx, div { "Current stopwatch time: {sec_elapsed}" })
|
||||
}
|
||||
```
|
||||
|
||||
Using asynchronous code can be difficult! This is just scratching the surface of what's possible. We have an entire chapter on using async properly in your Dioxus Apps.
|
||||
|
||||
### How do I tell Dioxus that my state changed?
|
||||
|
||||
So far, we've only updated our state with `.set`. However, you might've noticed that we used `AddAssign` to increment the `sec_elapsed` value in our stopwatch example *without* calling set. This is because the `AddAssign` trait is implemented for `UseState<T>` (the wrapper around our value returned from `use_state`). Under the hood, whenever you try to mutate our value through `UseState`, you're actually calling `.set` which informs Dioxus that _this_ component needs to be updated on the screen.
|
||||
|
||||
Whenever you inform Dioxus that the component needs to be updated, it will "render" your component again, storing the previous and current Elements in memory. Dioxus will automatically figure out the differences between the old and the new and generate a list of edits that the renderer needs to apply to change what's on the screen. This process is called "diffing":
|
||||
|
||||

|
||||
|
||||
In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`.
|
||||
|
||||
With these building blocks, we can craft new hooks similar to `use_state` that let us easily tell Dioxus that new information is ready to be sent to the screen.
|
||||
|
||||
### How do I update my state efficiently?
|
||||
|
||||
In general, Dioxus should be plenty fast for most use cases. However, there are some rules you should consider following to ensure your apps are quick.
|
||||
|
||||
- 1) **Don't call set—state _while rendering_**. This will cause Dioxus to unnecessarily re-check the component for updates.
|
||||
- 2) **Break your state apart into smaller sections.** Hooks are explicitly designed to "unshackle" your state from the typical model-view-controller paradigm, making it easy to reuse useful bits of code with a single function.
|
||||
- 3) **Move local state down**. Dioxus will need to re-check child components of your app if the root component is constantly being updated. You'll get best results if rapidly-changing state does not cause major re-renders.
|
||||
|
||||
Don't worry - Dioxus is fast. But, if your app needs *extreme performance*, then take a look at the `Performance Tuning` in the `Advanced Guides` book.
|
||||
|
||||
## Moving On
|
||||
|
||||
This overview was a lot of information - but it doesn't tell you everything!
|
||||
|
||||
In the next sections we'll go over:
|
||||
- `use_state` in depth
|
||||
- `use_ref` and other hooks
|
||||
- Handling user input
|
||||
@@ -0,0 +1 @@
|
||||
# Lifecycle, updates, and effects
|
||||
@@ -0,0 +1 @@
|
||||
# User Input and Controlled Components
|
||||
Reference in new issue
Block a user