Improve the custom hooks chapter

This commit is contained in:
Evan Almloff 2023-04-24 11:16:53 -05:00
parent 73a2beb327
commit 61dba2825c
5 changed files with 217 additions and 3 deletions

View file

@ -16,12 +16,12 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" }
dioxus-router = { path = "../../packages/router" }
dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] }
dioxus-tui = { path = "../../packages/dioxus-tui" }
fermi = { path = "../../packages/fermi" }
shipyard = "0.6.2"
# dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] }
fermi = { path = "../../packages/fermi" }
shipyard = "0.6.2"
serde = { version = "1.0.138", features=["derive"] }
reqwest = { version = "0.11.11", features = ["json"] }
tokio = { version = "1.19.2" , features=[]}
axum = { version = "0.6.1", features = ["ws"] }
gloo-storage = "0.2.2"

View file

@ -0,0 +1,38 @@
#![allow(unused)]
use dioxus::prelude::*;
fn main() {}
// ANCHOR: non_clone_state
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
struct UseState<'a, T> {
value: &'a RefCell<T>,
update: Arc<dyn Fn()>,
}
fn my_use_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> UseState<T> {
// The update function will trigger a re-render in the component cx is attached to
let update = cx.schedule_update();
// Create the initial state
let value = cx.use_hook(|| RefCell::new(init()));
UseState { value, update }
}
impl<T: Clone> UseState<'_, T> {
fn get(&self) -> T {
self.value.borrow().clone()
}
fn set(&self, value: T) {
// Update the state
*self.value.borrow_mut() = value;
// Trigger a re-render on the component the state is from
(self.update)();
}
}
// ANCHOR_END: non_clone_state

View file

@ -11,3 +11,57 @@ fn use_settings(cx: &ScopeState) -> &UseSharedState<AppSettings> {
use_shared_state::<AppSettings>(cx).expect("App settings not provided")
}
// ANCHOR_END: wrap_context
// ANCHOR: use_storage
use gloo_storage::{LocalStorage, Storage};
use serde::{de::DeserializeOwned, Serialize};
/// A persistent storage hook that can be used to store data across application reloads.
#[allow(clippy::needless_return)]
pub fn use_persistent<T: Serialize + DeserializeOwned + Default + 'static>(
cx: &ScopeState,
// A unique key for the storage entry
key: impl ToString,
// A function that returns the initial value if the storage entry is empty
init: impl FnOnce() -> T,
) -> &UsePersistent<T> {
// Use the use_ref hook to create a mutable state for the storage entry
let state = use_ref(cx, move || {
// This closure will run when the hook is created
let key = key.to_string();
let value = LocalStorage::get(key.as_str()).ok().unwrap_or_else(init);
StorageEntry { key, value }
});
// Wrap the state in a new struct with a custom API
// Note: We use use_hook here so that this hook is easier to use in closures in the rsx. Any values with the same lifetime as the ScopeState can be used in the closure without cloning.
cx.use_hook(|| UsePersistent {
inner: state.clone(),
})
}
struct StorageEntry<T> {
key: String,
value: T,
}
/// Storage that persists across application reloads
pub struct UsePersistent<T: 'static> {
inner: UseRef<StorageEntry<T>>,
}
impl<T: Serialize + DeserializeOwned + Clone + 'static> UsePersistent<T> {
/// Returns a reference to the value
pub fn get(&self) -> T {
self.inner.read().value.clone()
}
/// Sets the value
pub fn set(&self, value: T) {
let mut inner = self.inner.write();
// Write the new value to local storage
LocalStorage::set(inner.key.as_str(), &value);
inner.value = value;
}
}
// ANCHOR_END: use_storage

View file

@ -0,0 +1,57 @@
#![allow(unused)]
use dioxus::prelude::*;
fn main() {}
// ANCHOR: use_state
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone)]
struct UseState<T> {
value: Rc<RefCell<T>>,
update: Arc<dyn Fn()>,
}
fn my_use_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> &UseState<T> {
cx.use_hook(|| {
// The update function will trigger a re-render in the component cx is attached to
let update = cx.schedule_update();
// Create the initial state
let value = Rc::new(RefCell::new(init()));
UseState { value, update }
})
}
impl<T: Clone> UseState<T> {
fn get(&self) -> T {
self.value.borrow().clone()
}
fn set(&self, value: T) {
// Update the state
*self.value.borrow_mut() = value;
// Trigger a re-render on the component the state is from
(self.update)();
}
}
// ANCHOR_END: use_state
// ANCHOR: use_context
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
}
pub fn use_context_provider<T: 'static + Clone>(cx: &ScopeState, f: impl FnOnce() -> T) -> &T {
cx.use_hook(|| {
let val = f();
// Provide the context state to the scope
cx.provide_context(val.clone());
val
})
}
// ANCHOR_END: use_context

View file

@ -2,6 +2,8 @@
Hooks are a great way to encapsulate business logic. If none of the existing hooks work for your problem, you can write your own.
When writing your hook, you can make a function that accepts `cx: &ScopeState` as a parameter to accept a scope with any Props.
## Composing Hooks
To avoid repetition, you can encapsulate business logic based on existing hooks to create a new hook.
@ -12,6 +14,12 @@ For example, if many components need to access an `AppSettings` struct, you can
{{#include ../../../examples/hooks_composed.rs:wrap_context}}
```
Or if you want to wrap a hook that persists reloads with the storage API, you can build on top of the use_ref hook to work with mutable state:
```rust
{{#include ../../../examples/hooks_composed.rs:use_storage}}
```
## Custom Hook Logic
You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on!
@ -23,4 +31,61 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.
Inside the initialization closure, you will typically make calls to other `cx` methods. For example:
- The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes.
Here is a simplified implementation of the `use_state` hook:
```rust
{{#include ../../../examples/hooks_custom_logic.rs:use_state}}
```
- The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope
Here is an implementation of the `use_context` and `use_context_provider` hooks:
```rust
{{#include ../../../examples/hooks_custom_logic.rs:use_context}}
```
## Hook Anti-Patterns
When writing a custom hook, you should avoid the following anti-patterns:
- !Clone Hooks: To allow hooks to be used within async blocks, the hooks must be Clone. To make a hook clone, you can wrap data in Rc or Arc and avoid lifetimes in hooks.
This version of use_state may seem more efficient, but it is not cloneable:
```rust
{{#include ../../../examples/hooks_anti_patterns.rs:non_clone_state}}
```
If we try to use this hook in an async block, we will get a compile error:
```rust
fn FutureComponent(cx: &ScopeState) -> Element {
let my_state = my_use_state(cx, || 0);
cx.spawn({
to_owned![my_state];
async move {
my_state.set(1);
}
});
todo!()
}
```
But with the original version, we can use it in an async block:
```rust
fn FutureComponent(cx: &ScopeState) -> Element {
let my_state = use_state(cx, || 0);
cx.spawn({
to_owned![my_state];
async move {
my_state.set(1);
}
});
todo!()
}
```