freya_query/lib.rs
1//! # Freya Query
2//!
3//! A powerful, async-focused data management library for Freya applications,
4//! inspired by React Query. It handles caching, background refetching,
5//! deduplication, and automatic invalidation for async operations.
6//!
7//! It is available under the `query` feature flag of the `freya` crate:
8//!
9//! ```toml
10//! [dependencies]
11//! freya = { version = "...", features = ["query"] }
12//! ```
13//!
14//! Freya's built-in async primitives (`spawn`, `spawn_forever`, `use_future`)
15//! are great for *individual* async operations, but they don't share results
16//! between components, deduplicate concurrent calls, or invalidate stale data.
17//! Freya Query builds on top of those primitives and adds caching, background
18//! refetching, mutations, and invalidation, making it the right choice for
19//! fetching from an HTTP API, a database, or any other remote source.
20//!
21//! ## Overview
22//!
23//! Freya Query manages two types of operations:
24//!
25//! - **Queries** ([`use_query`](crate::query::use_query)): Read operations that
26//! fetch, cache, and reactively share data.
27//! - **Mutations** ([`use_mutation`](crate::mutation::use_mutation)): Write
28//! operations that modify data and can invalidate queries.
29//!
30//! ## Key Features
31//!
32//! - **Automatic Caching**: Query results are cached and reused across components.
33//! - **Background Refetching**: Stale data is automatically refreshed in the background.
34//! - **Invalidation**: Mutations can invalidate related queries to keep data fresh.
35//! - **Deduplication**: Multiple identical queries are automatically deduplicated.
36//! - **Error Handling**: Built-in error states.
37//! - **Reactive**: Integrates seamlessly with Freya's reactive state system.
38//!
39//! ## Queries
40//!
41//! ### Defining a query
42//!
43//! Implement [`QueryCapability`](crate::query::QueryCapability) on a type to
44//! define how data is fetched:
45//!
46//! ```rust,no_run
47//! # use freya::prelude::*;
48//! # use freya::query::*;
49//! #[derive(Clone, PartialEq, Hash, Eq)]
50//! struct FetchUser;
51//!
52//! impl QueryCapability for FetchUser {
53//! type Ok = String;
54//! type Err = String;
55//! type Keys = u32;
56//!
57//! async fn run(&self, user_id: &Self::Keys) -> Result<Self::Ok, Self::Err> {
58//! // Fetch from an API, database, etc.
59//! Ok(format!("User {user_id}"))
60//! }
61//! }
62//! ```
63//!
64//! ### Using a query in a component
65//!
66//! Call [`use_query`](crate::query::use_query) with a [`Query`](crate::query::Query) to subscribe
67//! a component to cached, reactive data:
68//!
69//! ```rust,no_run
70//! # use freya::prelude::*;
71//! # use freya::query::*;
72//! # #[derive(Clone, PartialEq, Hash, Eq)]
73//! # struct FetchUser;
74//! # impl QueryCapability for FetchUser {
75//! # type Ok = String;
76//! # type Err = String;
77//! # type Keys = u32;
78//! # async fn run(&self, user_id: &Self::Keys) -> Result<Self::Ok, Self::Err> {
79//! # Ok(format!("User {user_id}"))
80//! # }
81//! # }
82//! #[derive(PartialEq)]
83//! struct UserProfile(u32);
84//!
85//! impl Component for UserProfile {
86//! fn render(&self) -> impl IntoElement {
87//! let query = use_query(Query::new(self.0, FetchUser));
88//!
89//! match &*query.read().state() {
90//! QueryStateData::Pending => "Loading...".to_string(),
91//! QueryStateData::Loading { res } => {
92//! format!("Refreshing... Previous: {:?}", res)
93//! }
94//! QueryStateData::Settled { res, .. } => {
95//! format!("Result: {:?}", res)
96//! }
97//! }
98//! }
99//! }
100//! ```
101//!
102//! Multiple components using the same query (same capability type + same keys) share the
103//! same cache entry. The query only runs once and all subscribers receive the result.
104//!
105//! ### Reading query state
106//!
107//! [`UseQuery`](crate::query::UseQuery) gives access to the query state, see its docs for
108//! the full API. The state is exposed as [`QueryStateData`](crate::query::QueryStateData).
109//!
110//! ### Query configuration
111//!
112//! [`Query`](crate::query::Query) supports builder methods to control caching behavior.
113//! See its docs for the full list of options (`stale_time`, `clean_time`, `interval_time`, `enable`).
114//!
115//! ```rust,no_run
116//! # use freya::prelude::*;
117//! # use freya::query::*;
118//! # use std::time::Duration;
119//! # #[derive(Clone, PartialEq, Hash, Eq)]
120//! # struct FetchUser;
121//! # impl QueryCapability for FetchUser {
122//! # type Ok = String;
123//! # type Err = String;
124//! # type Keys = u32;
125//! # async fn run(&self, _: &Self::Keys) -> Result<Self::Ok, Self::Err> { Ok(String::new()) }
126//! # }
127//! # #[derive(PartialEq)]
128//! # struct Example;
129//! # impl Component for Example {
130//! # fn render(&self) -> impl IntoElement {
131//! let user = use_query(
132//! Query::new(1, FetchUser)
133//! .stale_time(Duration::from_secs(300))
134//! .clean_time(Duration::from_secs(600))
135//! .interval_time(Duration::from_secs(30))
136//! .enable(true),
137//! );
138//! # rect()
139//! # }
140//! # }
141//! ```
142//!
143//! ### Invalidating queries
144//!
145//! You can manually trigger a re-fetch from a [`UseQuery`](crate::query::UseQuery):
146//!
147//! ```rust,ignore
148//! // Fire-and-forget (spawns a background task)
149//! user.invalidate();
150//!
151//! // Await the result
152//! let result = user.invalidate_async().await;
153//! ```
154//!
155//! For broader invalidation, use [`QueriesStorage`](crate::query::QueriesStorage):
156//!
157//! ```rust,ignore
158//! // Re-run ALL FetchUser queries
159//! QueriesStorage::<FetchUser>::invalidate_all().await;
160//!
161//! // Re-run only FetchUser queries matching specific keys
162//! QueriesStorage::<FetchUser>::invalidate_matching(user_id).await;
163//! ```
164//!
165//! `invalidate_matching` calls the [`matches()`](crate::query::QueryCapability::matches) method on each
166//! cached query. By default `matches()` returns `true` (all queries match). Override it for selective invalidation:
167//!
168//! ```rust,no_run
169//! # use freya::prelude::*;
170//! # use freya::query::*;
171//! #[derive(Clone, PartialEq, Hash, Eq)]
172//! struct FetchUser {
173//! user_id: u32,
174//! }
175//!
176//! impl QueryCapability for FetchUser {
177//! type Ok = String;
178//! type Err = String;
179//! type Keys = u32;
180//!
181//! async fn run(&self, user_id: &Self::Keys) -> Result<Self::Ok, Self::Err> {
182//! Ok(format!("User {user_id}"))
183//! }
184//!
185//! fn matches(&self, keys: &Self::Keys) -> bool {
186//! // Only invalidate if the user_id matches
187//! &self.user_id == keys
188//! }
189//! }
190//! ```
191//!
192//! ### Standalone queries
193//!
194//! To run a query outside of a component (e.g. from an async task), use
195//! [`QueriesStorage::get()`](crate::query::QueriesStorage::get) with a [`GetQuery`](crate::query::GetQuery):
196//!
197//! ```rust,ignore
198//! let result = QueriesStorage::<FetchUser>::get(
199//! GetQuery::new(user_id, FetchUser)
200//! .stale_time(Duration::from_secs(60))
201//! ).await;
202//! ```
203//!
204//! ## Mutations
205//!
206//! ### Defining a mutation
207//!
208//! Implement [`MutationCapability`](crate::mutation::MutationCapability) to define a write operation:
209//!
210//! ```rust,no_run
211//! # use freya::prelude::*;
212//! # use freya::query::*;
213//! # #[derive(Clone, PartialEq, Hash, Eq)]
214//! # struct FetchUser;
215//! # impl QueryCapability for FetchUser {
216//! # type Ok = String;
217//! # type Err = String;
218//! # type Keys = u32;
219//! # async fn run(&self, _: &Self::Keys) -> Result<Self::Ok, Self::Err> { Ok(String::new()) }
220//! # }
221//! #[derive(Clone, PartialEq, Hash, Eq)]
222//! struct UpdateUser {
223//! user_id: u32,
224//! }
225//!
226//! impl MutationCapability for UpdateUser {
227//! type Ok = ();
228//! type Err = String;
229//! /// (field_name, new_value)
230//! type Keys = (String, String);
231//!
232//! async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
233//! // Send update to the API
234//! Ok(())
235//! }
236//!
237//! /// Called after `run()` completes. Use this to invalidate related queries.
238//! async fn on_settled(&self, _keys: &Self::Keys, result: &Result<Self::Ok, Self::Err>) {
239//! if result.is_ok() {
240//! QueriesStorage::<FetchUser>::invalidate_matching(self.user_id).await;
241//! }
242//! }
243//! }
244//! ```
245//!
246//! The [`on_settled`](crate::mutation::MutationCapability::on_settled) callback is the primary mechanism for
247//! keeping query data consistent after mutations. It runs after `run()` regardless of success or failure.
248//!
249//! ### Using a mutation in a component
250//!
251//! Call [`use_mutation`](crate::mutation::use_mutation) with a [`Mutation`](crate::mutation::Mutation):
252//!
253//! ```rust,no_run
254//! # use freya::prelude::*;
255//! # use freya::query::*;
256//! # #[derive(Clone, PartialEq, Hash, Eq)]
257//! # struct UpdateUser { user_id: u32 }
258//! # impl MutationCapability for UpdateUser {
259//! # type Ok = ();
260//! # type Err = String;
261//! # type Keys = (String, String);
262//! # async fn run(&self, _: &Self::Keys) -> Result<Self::Ok, Self::Err> { Ok(()) }
263//! # }
264//! #[derive(PartialEq)]
265//! struct UserEditor(u32);
266//!
267//! impl Component for UserEditor {
268//! fn render(&self) -> impl IntoElement {
269//! let mutation = use_mutation(Mutation::new(UpdateUser { user_id: self.0 }));
270//!
271//! let status = match &*mutation.read().state() {
272//! MutationStateData::Pending => "Ready",
273//! MutationStateData::Loading { .. } => "Saving...",
274//! MutationStateData::Settled { res, .. } if res.is_ok() => "Saved!",
275//! MutationStateData::Settled { .. } => "Error",
276//! };
277//!
278//! rect().child(status).child(
279//! Button::new()
280//! .on_press(move |_| mutation.mutate(("name".to_string(), "Alice".to_string())))
281//! .child("Save"),
282//! )
283//! }
284//! }
285//! ```
286//!
287//! See [`UseMutation`](crate::mutation::UseMutation) docs for the full API. The state is exposed
288//! as [`MutationStateData`](crate::mutation::MutationStateData).
289//!
290//! [`Mutation`](crate::mutation::Mutation) also supports builder methods, see its docs.
291//!
292//! ## Captured values
293//!
294//! Query and mutation types must implement `PartialEq` and `Hash` since they are used as cache keys.
295//! This is a problem for values like API clients or `State<T>` handles that should not affect cache identity.
296//!
297//! [`Captured<T>`](crate::captured::Captured) wraps a value so it is invisible to caching:
298//! its `PartialEq` always returns `true` and its `Hash` is a no-op.
299//!
300//! ```rust,no_run
301//! # use freya::prelude::*;
302//! # use freya::query::*;
303//! #[derive(Clone, PartialEq, Hash, Eq)]
304//! struct FetchTodos(Captured<State<DbClient>>);
305//!
306//! # #[derive(Clone)]
307//! # struct DbClient;
308//! impl QueryCapability for FetchTodos {
309//! type Ok = Vec<String>;
310//! type Err = String;
311//! type Keys = ();
312//!
313//! async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
314//! let _client: &State<DbClient> = &self.0;
315//! Ok(vec![])
316//! }
317//! }
318//! ```
319//!
320//! `Captured<T>` implements `Deref<Target = T>` and `DerefMut`, so you can use the inner
321//! value transparently.
322//!
323//! ## Examples
324//!
325//! - [`state_query.rs`](https://github.com/marc2332/freya/tree/main/examples/state_query.rs) - Basic query usage
326//! - [`state_mutation.rs`](https://github.com/marc2332/freya/tree/main/examples/state_mutation.rs) - Query + mutation with invalidation
327//! - [`hackernews.rs`](https://github.com/marc2332/freya/tree/main/examples/hackernews.rs) - Fetching from a real API with stale times
328//! - [`state_query_sqlite/`](https://github.com/marc2332/freya/tree/main/examples/state_query_sqlite) - Full CRUD app with SQLite
329
330pub mod captured;
331pub mod mutation;
332pub mod query;
333
334pub mod prelude {
335 pub use crate::{
336 captured::*,
337 mutation::*,
338 query::*,
339 };
340}