Skip to main content

freya_core/lifecycle/
future_task.rs

1use crate::prelude::*;
2pub enum FutureState<D> {
3    /// Has not started loading yet.
4    Pending,
5    /// Currently loading.
6    Loading,
7    /// Finished loading and has data.
8    Fulfilled(D),
9}
10
11impl<D> FutureState<D> {
12    pub fn ok(&self) -> Option<&D> {
13        if let Self::Fulfilled(d) = &self {
14            Some(d)
15        } else {
16            None
17        }
18    }
19
20    pub fn unwrap(&self) -> &D {
21        self.ok().expect("Future state is not fulfilled")
22    }
23
24    pub fn is_loading(&self) -> bool {
25        matches!(self, Self::Loading)
26    }
27
28    pub fn is_pending(&self) -> bool {
29        matches!(self, Self::Pending)
30    }
31}
32
33pub struct FutureTask<D, F> {
34    future: State<Box<dyn FnMut() -> F>>,
35    state: State<FutureState<D>>,
36    task: State<Option<TaskHandle>>,
37}
38
39impl<D, F> Clone for FutureTask<D, F> {
40    fn clone(&self) -> Self {
41        *self
42    }
43}
44
45impl<D, F> Copy for FutureTask<D, F> {}
46
47impl<D: 'static, F: Future<Output = D> + 'static> FutureTask<D, F> {
48    /// Create a [FutureTask] with the given callback.
49    pub fn create(future: impl FnMut() -> F + 'static) -> FutureTask<D, F> {
50        Self {
51            future: State::create(Box::new(future)),
52            state: State::create(FutureState::Pending),
53            task: State::create(None),
54        }
55    }
56
57    /// Create a [FutureTask] with a reactive callback. Any [State] read inside the
58    /// callback (outside the async block) subscribes it, restarting the future when it changes.
59    pub fn create_reactive(mut future: impl FnMut() -> F + 'static) -> FutureTask<D, F> {
60        let (notify, reactive_context) = ReactiveContext::new_for_task();
61        let mut future_task =
62            Self::create(move || ReactiveContext::run(reactive_context.clone(), &mut future));
63        spawn(async move {
64            loop {
65                future_task.start();
66                notify.notified().await;
67            }
68        });
69        future_task
70    }
71
72    /// Cancel the currently task if there is any.
73    pub fn cancel(&mut self) {
74        if let Some(task) = self.task.take() {
75            task.cancel();
76        }
77    }
78
79    /// Start the [FutureTask]. If it was running already then it will be restarted.
80    pub fn start(&mut self) {
81        self.cancel();
82        let mut this = *self;
83        let task = spawn(async move {
84            let future = this.future.write()();
85            this.state.set(FutureState::Loading);
86            let data = future.await;
87            this.state.set(FutureState::Fulfilled(data));
88        });
89        self.task.set(Some(task));
90    }
91
92    /// Read the state of the [FutureTask]. See [FutureState].
93    pub fn state(&self) -> ReadRef<'static, FutureState<D>> {
94        self.state.read()
95    }
96}
97
98/// Create a [FutureTask] with the given callback.
99///
100/// This is a hook around [spawn] that exposes the progress of the future as
101/// reactive state, so you can render the pending, loading and fulfilled cases
102/// without managing the task by hand. It starts polling automatically.
103///
104/// ```rust,no_run
105/// # use freya::prelude::*;
106/// #[derive(PartialEq)]
107/// struct Greeting;
108///
109/// impl Component for Greeting {
110///     fn render(&self) -> impl IntoElement {
111///         let future = use_future(|| async {
112///             // Some async work...
113///             "Hello!".to_string()
114///         });
115///
116///         match &*future.state() {
117///             FutureState::Pending | FutureState::Loading => "Loading...".to_string(),
118///             FutureState::Fulfilled(text) => text.clone(),
119///         }
120///     }
121/// }
122/// ```
123///
124/// The callback is reactive, any [State] read inside of it (outside the async block)
125/// subscribes the future, restarting it when that state changes. Reads inside the
126/// async block do not subscribe.
127///
128/// ```rust,no_run
129/// # use freya::prelude::*;
130/// # async fn load_user(user_id: usize) -> String { String::new() }
131/// # fn app() -> impl IntoElement {
132/// let user_id = use_state(|| 1);
133///
134/// // Restarts whenever `user_id` changes.
135/// let user = use_future(move || {
136///     let user_id = user_id();
137///     async move { load_user(user_id).await }
138/// });
139/// # let _ = user;
140/// # rect()
141/// # }
142/// ```
143///
144/// To read its state use [FutureTask::state].
145/// You may restart/stop it using [FutureTask::start] and [FutureTask::cancel].
146pub fn use_future<D: 'static, F: Future<Output = D> + 'static>(
147    future: impl FnMut() -> F + 'static,
148) -> FutureTask<D, F> {
149    use_hook(|| FutureTask::create_reactive(future))
150}