mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
docs: more docs and an errorhandling example
This commit is contained in:
parent
9846e0947b
commit
3acd643afb
8 changed files with 378 additions and 33 deletions
|
@ -2,16 +2,16 @@
|
|||
|
||||
![dioxuslogo](./images/dioxuslogo_full.png)
|
||||
|
||||
**Dioxus** is a framework and ecosystem for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get started with Dioxus running on the Web, Desktop, Mobile, and more.
|
||||
**Dioxus** is a library for building fast, scalable, and robust user interfaces with the Rust programming language. This guide will help you get started with Dioxus running on the Web, Desktop, Mobile, and more.
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope) -> Element {
|
||||
let (count, set_count) = use_state(&cx, || 0);
|
||||
let mut count = use_state(&cx, || 0);
|
||||
|
||||
cx.render(rsx!(
|
||||
h1 { "High-Five counter: {count}" }
|
||||
button { onclick: move |_| set_count(count + 1), "Up high!" }
|
||||
button { onclick: move |_| set_count(count - 1), "Down low!" }
|
||||
button { onclick: move |_| count += 1, "Up high!" }
|
||||
button { onclick: move |_| count -= 1, "Down low!" }
|
||||
))
|
||||
};
|
||||
```
|
||||
|
|
|
@ -62,10 +62,12 @@ Generally, here's the status of each platform:
|
|||
|
||||
|
||||
## Roadmap
|
||||
These Features are planned for the future of Dioxus:
|
||||
|
||||
---
|
||||
|
||||
|
||||
Core:
|
||||
### Core
|
||||
- [x] Release of Dioxus Core
|
||||
- [x] Upgrade documentation to include more theory and be more comprehensive
|
||||
- [ ] Support for HTML-side templates for lightning-fast dom manipulation
|
||||
|
@ -73,18 +75,18 @@ Core:
|
|||
- [ ] Support for ThreadSafe (Send + Sync)
|
||||
- [ ] Support for Portals
|
||||
|
||||
SSR
|
||||
### SSR
|
||||
- [x] SSR Support + Hydration
|
||||
- [ ] Integrated suspense support for SSR
|
||||
|
||||
Desktop
|
||||
### Desktop
|
||||
- [ ] Declarative window management
|
||||
- [ ] Templates for building/bundling
|
||||
- [ ] Fully native renderer
|
||||
- [ ] Access to Canvas/WebGL context natively
|
||||
|
||||
Mobile
|
||||
- [ ] Mobile standard library
|
||||
### Mobile
|
||||
- [ ] Mobile standard library
|
||||
- [ ] GPS
|
||||
- [ ] Camera
|
||||
- [ ] filesystem
|
||||
|
@ -93,22 +95,23 @@ Mobile
|
|||
- [ ] Bluetooth
|
||||
- [ ] Notifications
|
||||
- [ ] Clipboard
|
||||
- [ ]
|
||||
- [ ] Animations
|
||||
- [ ] Native Renderer
|
||||
|
||||
Bundling (CLI)
|
||||
- [x] translation from HTML into RSX
|
||||
- [ ] dev server
|
||||
- [ ] live reload
|
||||
- [ ] translation from JSX into RSX
|
||||
- [ ] hot module replacement
|
||||
- [ ] code splitting
|
||||
- [ ] asset macros
|
||||
- [ ] css pipeline
|
||||
- [ ] image pipeline
|
||||
### Bundling (CLI)
|
||||
- [x] Translation from HTML into RSX
|
||||
- [x] Dev server
|
||||
- [x] Live reload
|
||||
- [x] Translation from JSX into RSX
|
||||
- [ ] Hot module replacement
|
||||
- [ ] Code splitting
|
||||
- [ ] Asset macros
|
||||
- [ ] Css pipeline
|
||||
- [ ] Image pipeline
|
||||
|
||||
Essential hooks
|
||||
- [ ] Router
|
||||
- [ ] Global state management
|
||||
### Essential hooks
|
||||
- [x] Router
|
||||
- [x] Global state management
|
||||
- [ ] Resize observer
|
||||
|
||||
|
||||
|
|
|
@ -23,9 +23,8 @@
|
|||
<!-- - [Effects](interactivity/lifecycles.md) -->
|
||||
- [Managing State](state/index.md)
|
||||
- [Local State](state/localstate.md)
|
||||
- [Lifting State](state/liftingstate.md)
|
||||
- [Global State](state/sharedstate.md)
|
||||
- [Fanning Out](state/fanout.md)
|
||||
- [Lifting State](state/liftingstate.md)
|
||||
- [Error handling](state/errorhandling.md)
|
||||
- [Helper Crates](helpers/index.md)
|
||||
- [Fermi](state/fermi.md)
|
||||
|
|
|
@ -108,7 +108,7 @@ There are a few different approaches to choosing when to update your state. You
|
|||
|
||||
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.
|
||||
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 `onclick` 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 {
|
||||
|
@ -116,7 +116,7 @@ fn App(cx: Scope)-> Element {
|
|||
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
on_click: move |_| set_post(PostData::random())
|
||||
onclick: move |_| set_post(PostData::random())
|
||||
"Generate a random post"
|
||||
}
|
||||
Post { props: &post }
|
||||
|
@ -128,10 +128,10 @@ We'll dive much deeper into event listeners later.
|
|||
|
||||
### Updating state asynchronously
|
||||
|
||||
We can also update our state outside of event listeners with `futures` and `coroutines`.
|
||||
We can also update our state outside of event listeners with `futures` and `coroutines`.
|
||||
|
||||
- `Futures` are Rust's version of promises that can execute asynchronous work by an efficient polling system. We can submit new futures to Dioxus either through `push_future` which returns a `TaskId` or with `spawn`.
|
||||
- `Coroutines` are asynchronous blocks of our component that have the ability to cleanly interact with values, hooks, and other data in the component.
|
||||
- `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 and Futures 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.
|
||||
|
||||
|
|
|
@ -1,12 +1,158 @@
|
|||
# Error handling
|
||||
|
||||
A selling point of Rust for web development is the reliability of always knowing where errors can occur and being forced to handle them
|
||||
|
||||
However, we haven't talked about error handling at all in this guide! In this chapter, we'll cover some strategies in handling errors to ensure your app never crashes.
|
||||
|
||||
|
||||
|
||||
## The simplest - returning None
|
||||
|
||||
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 {
|
||||
fn App(cx: Scope) -> Element {
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
> This section is currently under construction! 🏗
|
||||
This lets us add in some syntactic sugar for operations we think *shouldn't* fail, but we're still not confident enough to "unwrap" on.
|
||||
|
||||
> The nature of `Option<VNode>` might change in the future as the `try` trait gets upgraded.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> Element {
|
||||
// immediately return "None"
|
||||
let name = cx.use_hook(|_| Some("hi"))?;
|
||||
}
|
||||
```
|
||||
|
||||
## Early return on result
|
||||
|
||||
Because Rust can't accept both Options and Results with the existing try infrastructure, you'll need to manually handle Results. This can be done by converting them into Options or by explicitly handling them.
|
||||
|
||||
```rust
|
||||
fn App(cx: Scope) -> Element {
|
||||
// Convert Result to Option
|
||||
let name = cx.use_hook(|_| "1.234").parse().ok()?;
|
||||
|
||||
|
||||
// Early return
|
||||
let count = cx.use_hook(|_| "1.234");
|
||||
let val = match count.parse() {
|
||||
Ok(val) => val
|
||||
Err(err) => return cx.render(rsx!{ "Parsing failed" })
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Notice that while hooks in Dioxus do not like being called in conditionals or loops, they *are* okay with early returns. Returning an error state early is a completely valid way of handling errors.
|
||||
|
||||
|
||||
## Match results
|
||||
|
||||
The next "best" way of handling errors in Dioxus is to match on the error locally. This is the most robust way of handling errors, though it doesn't scale to architectures beyond a single component.
|
||||
|
||||
To do this, we simply have an error state built into our component:
|
||||
|
||||
```rust
|
||||
let err = use_state(&cx, || None);
|
||||
```
|
||||
|
||||
Whenever we perform an action that generates an error, we'll set that error state. We can then match on the error in a number of ways (early return, return Element, etc).
|
||||
|
||||
|
||||
```rust
|
||||
fn Commandline(cx: Scope) -> Element {
|
||||
let error = use_state(&cx, || None);
|
||||
|
||||
cx.render(match *error {
|
||||
Some(error) => rsx!(
|
||||
h1 { "An error occured" }
|
||||
)
|
||||
None => rsx!(
|
||||
input {
|
||||
oninput: move |_| error.set(Some("bad thing happened!")),
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Passing error states through components
|
||||
|
||||
If you're dealing with a handful of components with minimal nesting, you can just pass the error handle into child components.
|
||||
|
||||
```rust
|
||||
fn Commandline(cx: Scope) -> Element {
|
||||
let error = use_state(&cx, || None);
|
||||
|
||||
if let Some(error) = **error {
|
||||
return cx.render(rsx!{ "An error occured" });
|
||||
}
|
||||
|
||||
cx.render(rsx!{
|
||||
Child { error: error.clone() }
|
||||
Child { error: error.clone() }
|
||||
Child { error: error.clone() }
|
||||
Child { error: error.clone() }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Much like before, our child components can manually set the error during their own actions. The advantage to this pattern is that we can easily isolate error states to a few components at a time, making our app more predictable and robust.
|
||||
|
||||
## Going global
|
||||
|
||||
A strategy for handling cascaded errors in larger apps is through signaling an error using global state. This particular pattern involves creating an "error" context, and then setting it wherever relevant. This particular method is not as "sophisticated" as React's error boundary, but it is more fitting for Rust.
|
||||
|
||||
To get started, consider using a built-in hook like `use_context` and `use_context_provider` or Fermi. Of course, it's pretty easy to roll your own hook too.
|
||||
|
||||
At the "top" of our architecture, we're going to want to explicitly declare a value that could be an error.
|
||||
|
||||
|
||||
```rust
|
||||
enum InputError {
|
||||
None,
|
||||
TooLong,
|
||||
TooShort,
|
||||
}
|
||||
|
||||
static INPUT_ERROR: Atom<InputError> = |_| InputError::None;
|
||||
```
|
||||
|
||||
Then, in our top level component, we want to explicitly handle the possible error state for this part of the tree.
|
||||
|
||||
```rust
|
||||
fn TopLevel(cx: Scope) -> Element {
|
||||
let error = use_read(&cx, INPUT_ERROR);
|
||||
|
||||
match error {
|
||||
TooLong => return cx.render(rsx!{ "FAILED: Too long!" }),
|
||||
TooShort => return cx.render(rsx!{ "FAILED: Too Short!" }),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now, whenever a downstream component has an error in its actions, it can simply just set its own error state:
|
||||
|
||||
```rust
|
||||
fn Commandline(cx: Scope) -> Element {
|
||||
let set_error = use_set(&cx, INPUT_ERROR);
|
||||
|
||||
cx.render(rsx!{
|
||||
input {
|
||||
oninput: move |evt| {
|
||||
if evt.value.len() > 20 {
|
||||
set_error(InputError::TooLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This approach to error handling is best in apps that have "well defined" error states. Consider using a crate like `thiserror` or `anyhow` to simplify the generation of the error types.
|
||||
|
||||
This pattern is widely popular in many contexts and is particularly helpful whenever your code generates a non-recoverable error. You can gracefully capture these "global" error states without panicking or mucking up state.
|
||||
|
|
|
@ -1,4 +1,131 @@
|
|||
# Lifting State
|
||||
# Lifting State and Fanning Out
|
||||
|
||||
Maintaining state local to components doesn't always work.
|
||||
|
||||
One of the most reliable state management patterns in large Dioxus apps is to `lift-up` and `fan-out`. Lifting up and fanning-out state is the ideal way to structure your app to maximize code reuse, testability, and deterministic rendering.
|
||||
|
||||
|
||||
> This section is currently under construction! 🏗
|
||||
## Lifting State
|
||||
|
||||
When building complex apps with Dioxus, the best approach is to start by placing your state and an UI all within a single component. Once your component has passed a few hundred lines, then it might be worth refactoring it into a few smaller components.
|
||||
|
||||
Here, we're now challenged with how to share state between these various components.
|
||||
|
||||
Let's say we refactored our component to separate an input and a display.
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!{
|
||||
Title {}
|
||||
Input {}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Whenever a value is inputted in our `Input` component, we need to somehow propagate those changes into the `Title` component.
|
||||
|
||||
A quick-and-dirty solution - which works for many apps - is to simply share a UseState between the two components.
|
||||
|
||||
```rust
|
||||
fn app(cx: Scope) -> Element {
|
||||
let text = use_state(&cx, || "default".to_string());
|
||||
|
||||
cx.render(rsx!{
|
||||
Title { text: text.clone() }
|
||||
Input { text: text.clone() }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
> Note: since we `Cloned` our `text` `UseState` handle, both `Title` and `Input` will be memoized.
|
||||
|
||||
Here, we've "lifted" state out of our `Input` component and brought it up to the closest shared ancestor. In our input component, we can directly use this UseState handle as if it had been defined locally:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn Input(cx: Scope, text: UseState<String>) -> Element {
|
||||
cx.render(rsx!{
|
||||
input { oninput: move |evt| text.set(evt.value.clone()) }
|
||||
})
|
||||
}
|
||||
```
|
||||
Similarly, our `Title` component would be straightforward:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn Title(cx: Scope, text: UseState<String>) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{text}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For more complicated use cases, we can take advantage of the EventHandler coercion talked about before to pass in any callback. Recall that fields on components that start with "on" are automatically "upgraded" into an `EventHandler` at the call site.
|
||||
|
||||
This lets us abstract over the exact type of state being used to store the data.
|
||||
|
||||
For the `Input` component, we would simply add a new `oninput` field:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn Input<'a>(cx: Scope<'a>, oninput: EventHandler<'a, String>) -> Element {
|
||||
cx.render(rsx!{
|
||||
input { oninput: move |evt| oninput.call(evt.value.clone()), }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For our `Title` component, we could also abstract it to take any `&str`:
|
||||
|
||||
```rust
|
||||
#[inline_props]
|
||||
fn Title<'a>(cx: Scope<'a>, text: &'a str) -> Element {
|
||||
cx.render(rsx!{
|
||||
h1 { "{text}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Fanning Out
|
||||
|
||||
As your app grows and grows, you might need to start pulling in global state to avoid prop drilling. This tends to solve a lot of problems, but generates even more.
|
||||
|
||||
For instance, let's say we built a beautiful `Title` component. Instead of passing props in, we instead are using a `use_read` hook from Fermi.
|
||||
|
||||
```rust
|
||||
fn Title(cx: Scope) -> Element {
|
||||
let title = use_read(&cx, TITLE);
|
||||
|
||||
cx.render(rsx!{
|
||||
h1 { "{title}" }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This is great - all is well in the world. We get precise updates, automatic memoization, and a solid abstraction. But, what happens when we want to reuse this component in another project? This component is now deeply intertwined with our global state - which might not be the same in another app.
|
||||
|
||||
In this case, we want to "lift" our global state out of "view" components. With `lifting`, our individual components at "leaf" position of our VirtualDom are "pure", making them easily reusable, testable, and deterministic. For instance, the "title" bar of our app might be a fairly complicated component.
|
||||
|
||||
|
||||
To enable our title component to be used across apps, we want to lift our atoms upwards and out of the Title component. We would organize a bunch of other components in this section of the app to share some of the same state.
|
||||
|
||||
```rust
|
||||
fn DocsiteTitlesection(cx: Scope) {
|
||||
// Use our global state in a wrapper component
|
||||
let title = use_read(&cx, TITLE);
|
||||
let subtitle = use_read(&cx, SUBTITLE);
|
||||
|
||||
let username = use_read(&cx, USERNAME);
|
||||
let points = use_read(&cx, POINTS);
|
||||
|
||||
// and then pass our global state in from the outside
|
||||
cx.render(rsx!{
|
||||
Title { title: title.clone(), subtitle: subtitle.clone() }
|
||||
User { username: username.clone(), points: points.clone() }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This particular wrapper component unfortunately cannot be reused across apps. However, because it simply wraps other real elements, it doesn't have to. We are free to reuse our TitleBar and UserBar components across apps with ease. We also know that this particular component is plenty performant because the wrapper doesn't have any props and is always memoized. The only times this component re-renders is when any of the atoms change.
|
||||
|
||||
This is the beauty of Dioxus - we always know where our components are likely to be re-rendered. Our wrapper components can easily prevent any large re-renders by simply memoizing their components. This system might not be as elegant or precise as signal systems found in libraries like Sycamore or SolidJS, but it is quite ergonomic.
|
||||
|
|
|
@ -60,3 +60,51 @@ struct Todo {
|
|||
```
|
||||
|
||||
This is not only better for encapsulation and abstraction, but it's only more performant! Whenever a Todo is hovered, we won't need to re-render *every* Todo again - only the Todo that's currently being hovered.
|
||||
|
||||
|
||||
Wherever possible, you should try to refactor the "view" layer *out* of your data model.
|
||||
|
||||
## Immutability
|
||||
|
||||
Storing all of your state inside a `use_ref` might be tempting. However, you'll quickly run into an issue where the `Ref` provided by `read()` "does not live long enough." An indeed - you can't return locally-borrowed value into the Dioxus tree.
|
||||
|
||||
In these scenarios consider breaking your `use_ref` into individual `use_state`s.
|
||||
|
||||
You might've started modeling your component with a struct and use_ref
|
||||
|
||||
```rust
|
||||
struct State {
|
||||
count: i32,
|
||||
color: &'static str,
|
||||
names: HashMap<String, String>
|
||||
}
|
||||
|
||||
// in the component
|
||||
let state = use_ref(&cx, State::new)
|
||||
```
|
||||
|
||||
The "better" approach for this particular component would be to break the state apart into different values:
|
||||
|
||||
```rust
|
||||
let count = use_state(&cx, || 0);
|
||||
let color = use_state(&cx, || "red");
|
||||
let names = use_state(&cx, HashMap::new);
|
||||
```
|
||||
|
||||
You might recognize that our "names" value is a HashMap - which is not terribly cheap to clone every time we update its value. To solve this issue, we *highly* suggest using a library like [`im`](https://crates.io/crates/im) which will take advantage of structural sharing to make clones and mutations much cheaper.
|
||||
|
||||
When combined with the `make_mut` method on `use_state`, you can get really succinct updates to collections:
|
||||
|
||||
```rust
|
||||
let todos = use_state(&cx, im_rc::HashMap::default);
|
||||
|
||||
todos.make_mut().insert("new todo", Todo {
|
||||
contents: "go get groceries",
|
||||
});
|
||||
```
|
||||
|
||||
## Moving on
|
||||
|
||||
This particular local patterns are powerful but is not a cure-all for state management problems. Sometimes your state problems are much larger than just staying local to a component.
|
||||
|
||||
|
||||
|
|
22
examples/error_handle.rs
Normal file
22
examples/error_handle.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let val = use_state(&cx, || "0.0001");
|
||||
|
||||
let num = match val.parse::<f32>() {
|
||||
Err(_) => return cx.render(rsx!("Parsing failed")),
|
||||
Ok(num) => num,
|
||||
};
|
||||
|
||||
cx.render(rsx! {
|
||||
h1 { "The parsed value is {num}" }
|
||||
button {
|
||||
onclick: move |_| val.set("invalid"),
|
||||
"Set an invalid number"
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue