sycamore_query/
lib.rs

1//! Provides `react-query`/`tanstack-query` style hooks for `sycamore`.
2//! I aim to eventually have mostly feature parity, but the project is currently
3//! in an MVP (minimum viable product) state. This means the basic functionality
4//! works (caching, background fetching, invalidations, mutations, refetching),
5//! but most of the configurability and automatic refetching on window events
6//! is missing. If you need a specific feature or configuration option, feel
7//! free to open an issue or even a PR and I'll know to prioritise it.
8//!
9//! # Usage
10//!
11//! To use the library you need to provide it with a [`QueryClient`] as a context.
12//! This is ideally done in your top level component or index view so your cache
13//! is global. If you want to have separate caches for different parts of your
14//! app it could make sense to set multiple [`QueryClient`]s.
15//!
16//! ```
17//! # use sycamore::prelude::*;
18//! use sycamore_query::{QueryClient, ClientOptions};
19//!
20//! #[component]
21//! pub fn App<G: Html>(cx: Scope) -> View<G> {
22//!     provide_context(cx, QueryClient::new(ClientOptions::default()));
23//!     
24//!     view! { cx, }
25//! }
26//! ```
27//!
28//! Now you can use [`use_query`](crate::query::use_query) and
29//! [`use_mutation`](crate::mutation::use_mutation) from any of your components.
30//!
31//! ```
32//! # use sycamore::prelude::*;
33//! # use sycamore_query::{QueryClient, ClientOptions};
34//! use sycamore_query::prelude::*;
35//!
36//! # mod api {
37//! #   use std::rc::Rc;
38//! #   pub async fn hello(name: Rc<String>) -> Result<String, String> {
39//! #       Ok(name.to_string())
40//! #   }
41//! # }
42//!
43//! #[component]
44//! pub fn Hello<G: Html>(cx: Scope) -> View<G> {
45//! #   provide_context(cx, QueryClient::new(ClientOptions::default()));
46//!     let name = create_rc_signal("World".to_string());
47//!     let Query { data, status, refetch } = use_query(
48//!         cx,
49//!         ("hello", name.get()),
50//!         move || api::hello(name.get())
51//!     );
52//!
53//!     match data.get_data() {
54//!         QueryData::Loading => view! { cx, p { "Loading..." } },
55//!         QueryData::Ok(message) => view! { cx, p { (message) } },
56//!         QueryData::Err(err) => view! { cx, p { "An error has occured: " } p { (err) } }
57//!     }
58//! }
59//! ```
60//!
61//! This will fetch the data in the background and handle all sorts of things
62//! for you: retrying on error (up to 3 times by default), caching, updating when
63//! a mutation invalidates the query or another query with the same key fetches
64//! the data, etc.
65//!
66//! # More information
67//!
68//! I don't have the time to write an entire book on this library right now, so just
69//! check out the `react-query` docs and the type level docs for Rust-specific
70//! details, keeping in mind only a subset of `react-query` is currently implemented.
71
72#![warn(missing_docs)]
73
74use std::{
75    any::Any,
76    future::Future,
77    hash::{Hash, Hasher},
78    pin::Pin,
79    rc::Rc,
80};
81
82use fnv::FnvHasher;
83use sycamore::reactive::{RcSignal, ReadSignal, Signal};
84
85mod cache;
86mod client;
87/// Mutation related functions and types
88pub mod mutation;
89/// Query related functions and types
90pub mod query;
91
92/// The sycamore-query prelude.
93///
94/// In most cases, it is idiomatic to use a glob import (aka wildcard import) at the beginning of
95/// your Rust source file.
96///
97/// ```rust
98/// use sycamore_query::prelude::*;
99/// ```
100pub mod prelude {
101    pub use crate::mutation::{use_mutation, Mutation};
102    pub use crate::query::{use_query, Query};
103    pub use crate::{keys, AsKeySignal, AsRcKeySignal, QueryData, QuerySignalExt, Status};
104}
105
106pub use client::*;
107
108pub(crate) type Fetcher =
109    Rc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<Rc<dyn Any>, Rc<dyn Any>>>>>>;
110pub(crate) type DataSignal = Signal<QueryData<Rc<dyn Any>, Rc<dyn Any>>>;
111
112/// Trait for anything that can be turned into a key
113/// The reason this exists is to allow for prefix invalidation, so lists or
114/// tuples should return one hash per element.
115/// It's automatically implemented for `String`, `str` and any tuple of size
116/// 2 - 12 where each element implements `Hash`.
117/// If your keys aren't covered by the default implementation for some reason,
118/// you can implement this manually.
119///
120/// # Example
121/// ```
122/// # use sycamore_query::AsKey;
123/// # use fnv::FnvHasher;
124/// # use std::hash::{Hasher, Hash};
125/// struct MyType {
126///     item1: String,
127///     item2: String,
128/// }
129///
130/// impl AsKey for MyType {
131///     fn as_key(&self) -> Vec<u64> {
132///         let mut hash = FnvHasher::default();
133///         self.item1.hash(&mut hash);
134///         let hash1 = hash.finish();
135///         hash = FnvHasher::default();
136///         self.item2.hash(&mut hash);
137///         let hash2 = hash.finish();
138///         vec![hash1, hash2]
139///     }
140/// }
141/// ```
142/// }
143pub trait AsKeys {
144    /// Internal function to convert the type to a key for use in the query cache
145    /// and notifier list.
146    fn as_keys(&self) -> Vec<u64>;
147}
148
149impl AsKeys for str {
150    fn as_keys(&self) -> Vec<u64> {
151        let mut hash = FnvHasher::default();
152        self.hash(&mut hash);
153        vec![hash.finish()]
154    }
155}
156
157impl AsKeys for &str {
158    fn as_keys(&self) -> Vec<u64> {
159        let mut hash = FnvHasher::default();
160        self.hash(&mut hash);
161        vec![hash.finish()]
162    }
163}
164
165impl AsKeys for String {
166    fn as_keys(&self) -> Vec<u64> {
167        self.as_str().as_keys()
168    }
169}
170
171macro_rules! impl_as_key_tuple {
172    ($($ty:ident),*) => {
173        impl<$($ty: Hash),*> AsKeys for ($($ty),*) {
174            fn as_keys(&self) -> Vec<u64> {
175                #[allow(non_snake_case)]
176                let ($($ty),*) = self;
177                vec![$(
178                    {
179                        let mut hash = FnvHasher::default();
180                        $ty.hash(&mut hash);
181                        hash.finish()
182                    }
183                ),*]
184            }
185        }
186    };
187}
188
189// Implement for tuples up to 12 long
190impl_as_key_tuple!(T1, T2);
191impl_as_key_tuple!(T1, T2, T3);
192impl_as_key_tuple!(T1, T2, T3, T4);
193impl_as_key_tuple!(T1, T2, T3, T4, T5);
194impl_as_key_tuple!(T1, T2, T3, T4, T5, T6);
195impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7);
196impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
197impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
198impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
199impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
200impl_as_key_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
201
202/// The data type of a query.
203///
204/// # States
205///
206/// * `Loading` - No query data is available yet
207/// * `Ok` - Query data was successfully fetched and is available. Note this
208/// might be stale data, check `QueryStatus` if you need to verify whether the
209/// query is currently fetching fresh data.
210/// * `Err` - Query data still wasn't able to be fetched after the retry strategy
211/// was exhausted. This contains the backing error.
212///
213#[derive(Clone)]
214pub enum QueryData<T, E> {
215    /// No query data is available yet
216    Loading,
217    /// Query data was successfully fetched and is available. Note this
218    /// might be stale data, check `QueryStatus` if you need to verify whether the
219    /// query is currently fetching fresh data.
220    Ok(T),
221    /// Query data still wasn't able to be fetched after the retry strategy
222    /// was exhausted. This contains the backing error.
223    Err(E),
224}
225
226/// The status of a query.
227///
228/// # States
229///
230/// * `Fetching` - Query data is currently being fetched. This might be because
231/// no data is available ([`QueryData::Loading`]) or because the data is
232/// considered stale.
233/// * `Success` - Query data is available and fresh.
234/// * `Idle` - Query is disabled from running.
235#[derive(Debug, PartialEq, Eq, Clone, Copy)]
236pub enum Status {
237    /// Query data is currently being fetched. This might be because
238    /// no data is available ([`QueryData::Loading`]) or because the data is
239    Fetching,
240    /// Query data is available and fresh.
241    Success,
242    /// Query is disabled from running.
243    Idle,
244}
245
246/// A convenience macro for passing a set of keys.
247/// Keys don't have the same type, so regular `Vec`s don't work.
248///
249/// # Example Usage
250///
251/// ```
252/// # use sycamore_query::keys;
253/// # use std::rc::Rc;
254/// # let client = sycamore_query::QueryClient::new(Default::default());
255/// client.invalidate_queries(keys![("hello", "World"), "test", ("user", 3)]);
256/// ```
257///
258#[macro_export]
259macro_rules! keys {
260    (@to_unit $($_:tt)*) => (());
261    (@count $($tail:expr),*) => (
262        <[()]>::len(&[$(keys!(@to_unit $tail)),*])
263      );
264
265    [$($key: expr),* $(,)?] => {
266        {
267            use $crate::AsKeys;
268            let mut res = ::std::vec::Vec::with_capacity(keys!(@count $($key),*));
269            $(
270                res.push($key.as_keys());
271            )*
272            res
273        }
274    };
275}
276
277/// Utility functions for dealing with QueryData in signals.
278pub trait QuerySignalExt<T, E> {
279    /// Unwraps the outer `Rc` of the signal to provide you with an easier to
280    /// match on, unwrapped [`QueryData`].
281    ///
282    /// # Example Usage
283    ///
284    /// ```
285    /// # use sycamore_query::{QueryData, QuerySignalExt};
286    /// # use sycamore::reactive::create_rc_signal;
287    /// # use std::rc::Rc;
288    /// # let signal = create_rc_signal::<QueryData<_, Rc<String>>>(QueryData::Ok(Rc::new("Hello".to_string())));
289    ///
290    /// match signal.get_data() {
291    ///     QueryData::Ok(message) => println!("{message}"),
292    ///     QueryData::Err(err) => eprintln!("{err}"),
293    ///     QueryData::Loading => println!("No data yet")
294    /// }
295    ///
296    /// ```
297    fn get_data(&self) -> QueryData<Rc<T>, Rc<E>>;
298}
299
300impl<T, E> QuerySignalExt<T, E> for ReadSignal<QueryData<Rc<T>, Rc<E>>> {
301    fn get_data(&self) -> QueryData<Rc<T>, Rc<E>> {
302        match self.get().as_ref() {
303            QueryData::Loading => QueryData::Loading,
304            QueryData::Ok(data) => QueryData::Ok(data.clone()),
305            QueryData::Err(err) => QueryData::Err(err.clone()),
306        }
307    }
308}
309
310struct MyRcSignal<T>(Rc<Signal<T>>);
311
312pub(crate) fn as_rc<T>(signal: RcSignal<T>) -> Rc<Signal<T>> {
313    // UNSAFE: This is actually kind of unsafe, but as long as the signature of
314    // `RcSignal` doesn't change and the compiler doesn't throw a curveball it
315    // should work. This should be replaced with a builtin way to do it.
316    let signal: MyRcSignal<T> = unsafe { std::mem::transmute(signal) };
317    signal.0
318}
319
320/// Internal type for tracking key changes. Only exposed because it's used in a public trait
321pub struct KeySignal<'cx, T: Hash>(&'cx ReadSignal<T>);
322/// Internal type for tracking key changes. Only exposed because it's used in a public trait
323pub struct RcKeySignal<T: Hash>(RcSignal<T>);
324
325/// Extension to allow for tracking key changes. If I can get some changes into sycamore this should
326/// become redundant
327///
328/// # Usage
329///
330/// ```
331/// # use sycamore::prelude::*;
332/// use sycamore_query::prelude::*;
333/// # #[component]
334/// # pub fn App<G: Html>(cx: Scope) -> View<G> {
335/// # async fn hello(s: String) -> Result<String, String> {
336/// #   Ok(s.to_string())
337/// # }
338///  let signal = create_signal(cx, "Test");
339/// // Updates every time signal changes
340/// use_query(cx, ("hello", signal.key()), move || hello(signal.get().to_string());
341/// # }
342/// ```
343pub trait AsKeySignal<T: Hash> {
344    /// Creates a reference to the signal that tracks when it's hashed (sycamore uses
345    /// [`get_untracked`](sycamore::reactive::ReadSignal) in the [`Hash`](std::hash::Hash)
346    /// implementation for signals).
347    fn key(&self) -> KeySignal<'_, T>;
348}
349
350/// Extension to allow for tracking key changes. If I can get some changes into sycamore this should
351/// become redundant
352///
353/// # Usage
354///
355/// ```
356/// # use sycamore::prelude::*;
357/// use sycamore_query::prelude::*;
358/// # #[component]
359/// # pub fn App<G: Html>(cx: Scope) -> View<G> {
360/// # async fn hello(s: String) -> Result<String, String> {
361/// #   Ok(s.to_string())
362/// # }
363/// let signal = create_rc_signal("Test");
364/// // Updates every time signal changes
365/// use_query(cx, ("hello", signal.clone().rc_key()), move || hello(signal.get().to_string());
366/// # }
367/// ```
368pub trait AsRcKeySignal<T: Hash> {
369    /// Creates a copy of the signal that tracks when it's hashed (sycamore uses `get_untracked`
370    /// in the `Hash` implementation for signals).
371    fn rc_key(self) -> RcKeySignal<T>;
372}
373
374impl<T: Hash> AsKeySignal<T> for ReadSignal<T> {
375    fn key(&self) -> KeySignal<'_, T> {
376        KeySignal(self)
377    }
378}
379
380impl<T: Hash> AsRcKeySignal<T> for RcSignal<T> {
381    fn rc_key(self) -> RcKeySignal<T> {
382        RcKeySignal(self)
383    }
384}
385
386impl<'cx, T: Hash> Hash for KeySignal<'cx, T> {
387    fn hash<H: Hasher>(&self, state: &mut H) {
388        self.0.track();
389        self.0.hash(state);
390    }
391}
392
393impl<T: Hash> Hash for RcKeySignal<T> {
394    fn hash<H: Hasher>(&self, state: &mut H) {
395        self.0.track();
396        self.0.hash(state);
397    }
398}