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}