dioxus_fullstack_hooks/hooks/
server_future.rs

1use dioxus_core::{suspend, use_hook, RenderError};
2use dioxus_hooks::*;
3use dioxus_signals::ReadableExt;
4use serde::{de::DeserializeOwned, Serialize};
5use std::future::Future;
6
7/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
8///
9///
10/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
11///
12///
13/// <div class="warning">
14///
15/// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
16///
17/// ```rust, no_run
18/// # use dioxus::prelude::*;
19/// // ❌ The future inside of use_server_future is not reactive
20/// let id = use_signal(|| 0);
21/// use_server_future(move || {
22///     async move {
23///          // But the future is not reactive which means that the future will not subscribe to any reads here
24///          println!("{id}");
25///     }
26/// });
27/// // ✅ The closure that creates the future for use_server_future is reactive
28/// let id = use_signal(|| 0);
29/// use_server_future(move || {
30///     // The closure itself is reactive which means the future will subscribe to any signals you read here
31///     let cloned_id = id();
32///     async move {
33///          // But the future is not reactive which means that the future will not subscribe to any reads here
34///          println!("{cloned_id}");
35///     }
36/// });
37/// ```
38///
39/// </div>
40///
41/// # Example
42///
43/// ```rust, no_run
44/// # use dioxus::prelude::*;
45/// # async fn fetch_article(id: u32) -> String { unimplemented!() }
46/// use dioxus::prelude::*;
47///
48/// fn App() -> Element {
49///     let mut article_id = use_signal(|| 0);
50///     // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
51///     // The future will rerun any time the
52///     // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
53///     let article = use_server_future(move || fetch_article(article_id()))?;
54///
55///     rsx! {
56///         "{article().unwrap()}"
57///     }
58/// }
59/// ```
60#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
61#[track_caller]
62pub fn use_server_future<T, F>(
63    mut future: impl FnMut() -> F + 'static,
64) -> Result<Resource<T>, RenderError>
65where
66    T: Serialize + DeserializeOwned + 'static,
67    F: Future<Output = T> + 'static,
68{
69    let serialize_context = use_hook(dioxus_fullstack_protocol::serialize_context);
70
71    // We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
72    #[allow(unused)]
73    let storage_entry: dioxus_fullstack_protocol::SerializeContextEntry<T> =
74        use_hook(|| serialize_context.create_entry());
75
76    #[cfg(feature = "server")]
77    let caller = std::panic::Location::caller();
78
79    // If this is the first run and we are on the web client, the data might be cached
80    #[cfg(feature = "web")]
81    let initial_web_result =
82        use_hook(|| std::rc::Rc::new(std::cell::RefCell::new(Some(storage_entry.get()))));
83
84    let resource = use_resource(move || {
85        #[cfg(feature = "server")]
86        let storage_entry = storage_entry.clone();
87
88        let user_fut = future();
89
90        #[cfg(feature = "web")]
91        let initial_web_result = initial_web_result.clone();
92
93        #[allow(clippy::let_and_return)]
94        async move {
95            // If this is the first run and we are on the web client, the data might be cached
96            #[cfg(feature = "web")]
97            match initial_web_result.take() {
98                // The data was deserialized successfully from the server
99                Some(Ok(o)) => return o,
100
101                // The data is still pending from the server. Don't try to resolve it on the client
102                Some(Err(dioxus_fullstack_protocol::TakeDataError::DataPending)) => {
103                    std::future::pending::<()>().await
104                }
105
106                // The data was not available on the server, rerun the future
107                Some(Err(_)) => {}
108
109                // This isn't the first run, so we don't need do anything
110                None => {}
111            }
112
113            // Otherwise just run the future itself
114            let out = user_fut.await;
115
116            // If this is the first run and we are on the server, cache the data in the slot we reserved for it
117            #[cfg(feature = "server")]
118            storage_entry.insert(&out, caller);
119
120            out
121        }
122    });
123
124    // On the first run, force this task to be polled right away in case its value is ready
125    use_hook(|| {
126        let _ = resource.task().poll_now();
127    });
128
129    // Suspend if the value isn't ready
130    if resource.state().cloned() == UseResourceState::Pending {
131        let task = resource.task();
132        if !task.paused() {
133            return Err(suspend(task).unwrap_err());
134        }
135    }
136
137    Ok(resource)
138}