dioxus
dioxus copied to clipboard
`use_request`: a `use_resource` alternative for manual trigger
Feature Request
I'd like to have a "use_resource" that allows me to trigger a query only when a button is pushed.
Currently, I'm using the following code to achieve this:
enum QueryState<R> {
Pending,
Fetching,
Result(R),
}
let mut signal_result = use_signal::<QueryState<Result<String, ServerFnError>>>(|| QueryState::Pending);
let request_string = move || {
*signal_result.write() = QueryState::Fetching;
spawn(async move {
let result = // perform a async server function;
*signal_result.write() = QueryState::Result(result);
});
};
rsx! {
button {
onclick: move |_| {
request_string();
},
"Click me"
}
}
The problem with this approach is that I have to keep track of all the state information by myself. Also, it's hard to use in multiple places and I feel that this is a good feature for Dioxus
I am also interested in this, but I suspect the answer to your question may simply be to convert what you have above into a hook that can then be reused. I had a discussion about this with Evan (in the midst of many other topics): https://discord.com/channels/899851952891002890/1238219898795196499/1242814582746124328 and he suggested looking at https://dioxuslabs.com/learn/0.5/cookbook/state/custom_hooks for doing what you (and I) described.
It's worth noting that use_resource already has some potentially relevant states: https://github.com/DioxusLabs/dioxus/blob/487570d89751b34bbfd5e9b5ff1e0fd3780bf332/packages/hooks/src/use_resource.rs#L132 and the difference is in the logic of how it automatically starts the async task immediately rather than waiting until some trigger.
Ok, so I'm gonna leave a few possibilities for this.
- Use
dioxus-queryhttps://github.com/marc2332/dioxus-query. It's more or less what I wanted with mutations. - This code adapted from
use_resourcecan be used, maybe added to library in the future:
use std::{
cell::Cell,
future::{self, Future},
mem::{self, MaybeUninit},
ops::Deref,
rc::Rc,
};
use dioxus::{logger::tracing, prelude::*};
use futures_util::{pin_mut, FutureExt};
pub struct UseRequest<T>
where
T: 'static,
{
callback: Callback<(), Task>,
state: Signal<RequestState<T>>,
}
impl<T> Clone for UseRequest<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for UseRequest<T> {}
impl<T> UseRequest<T> {
pub fn cancel(&mut self) {
self.state.write().cancel();
}
pub fn state(&self) -> Signal<RequestState<T>> {
self.state
}
pub fn fetching(&self) -> bool {
matches!(&*self.state().read(), RequestState::Fetching(_))
}
}
#[derive(Default, Clone, Copy)]
pub enum RequestState<T> {
#[default]
Pending,
Fetching(Task),
Result(T),
}
impl<T> RequestState<T> {
fn cancel(&mut self) {
if !matches!(self, Self::Fetching(_)) {
return;
}
let mut old_value = Self::Pending;
// switch current to pending
mem::swap(self, &mut old_value);
// cancel the old value
if let Self::Fetching(task) = old_value {
task.cancel();
}
}
}
pub fn use_request<T, Fut>(mut future: impl FnMut() -> Fut + 'static) -> UseRequest<T>
where
Fut: Future<Output = T> + 'static,
T: 'static,
{
let mut state = use_signal(|| RequestState::Pending);
let location = std::panic::Location::caller();
let (rc, _changed) = use_hook(|| {
let (rc, changed) = ReactiveContext::new_with_origin(location);
(rc, Rc::new(Cell::new(Some(changed))))
});
let cb = use_callback(move |_| {
// Create the user's task
let fut = rc.reset_and_run_in(&mut future);
// Spawn a wrapper task that polls the inner future and watch its dependencies
spawn(async move {
// move the future here and pin it so we can poll it
let fut = fut;
pin_mut!(fut);
// Run each poll in the context of the reactive scope
// This ensures the scope is properly subscribed to the future's dependencies
let res = future::poll_fn(|cx| {
rc.run_in(|| {
tracing::trace_span!("polling resource", location = %location)
.in_scope(|| fut.poll_unpin(cx))
})
})
.await;
// Set the value and state
state.set(RequestState::Result(res));
})
});
UseRequest {
callback: cb,
state,
}
}
impl<T> UseRequest<T> {
fn call(&self) {
let mut state = self.state;
if let RequestState::Fetching(_) = &*state.read() {
return;
}
let my_request = self.callback;
let task = my_request(());
*state.write() = RequestState::Fetching(task);
}
}
impl<T> Deref for UseRequest<T> {
type Target = dyn Fn();
fn deref(&self) -> &Self::Target {
let uninit_callable = MaybeUninit::<Self>::uninit();
let uninit_closure = move || Self::call(unsafe { &*uninit_callable.as_ptr() });
let size_of_closure = mem::size_of_val(&uninit_closure);
fn second<'a, T>(_a: &T, b: &'a T) -> &'a T {
b
}
#[allow(clippy::missing_transmute_annotations)]
let reference_to_closure = second(&uninit_closure, unsafe { mem::transmute(self) });
//#[allow(clippy::forget_non_drop)]
#[allow(forgetting_copy_types)]
mem::forget(uninit_closure);
assert_eq!(size_of_closure, mem::size_of::<Self>());
reference_to_closure as &dyn Fn()
}
}