dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

`use_request`: a `use_resource` alternative for manual trigger

Open gusinacio opened this issue 1 year ago • 1 comments
trafficstars

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

gusinacio avatar Jun 17 '24 16:06 gusinacio

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.

rogusdev avatar Jun 17 '24 19:06 rogusdev

Ok, so I'm gonna leave a few possibilities for this.

  1. Use dioxus-query https://github.com/marc2332/dioxus-query. It's more or less what I wanted with mutations.
  2. This code adapted from use_resource can 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()
    }
}

gusinacio avatar Jan 21 '25 20:01 gusinacio