Expand description
§Remote Data
Freya Query provides async data management for Freya applications, handling caching, background refetching, deduplication, and automatic invalidation.
It is available under the query feature flag:
[dependencies]
freya = { version = "...", features = ["query"] }It manages two types of operations:
- Queries (
use_query): Read operations that fetch, cache, and reactively share data. - Mutations (
use_mutation): Write operations that modify data and can invalidate queries.
§Queries
§Defining a query
Implement QueryCapability on a type to define how data is fetched:
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchUser;
impl QueryCapability for FetchUser {
type Ok = String;
type Err = String;
/// The input parameter for the query.
type Keys = u32;
async fn run(&self, user_id: &Self::Keys) -> Result<Self::Ok, Self::Err> {
// Fetch from an API, database, etc.
Ok(format!("User {user_id}"))
}
}§Using a query in a component
Call use_query with a Query to subscribe
a component to cached, reactive data:
#[derive(PartialEq)]
struct UserProfile(u32);
impl Component for UserProfile {
fn render(&self) -> impl IntoElement {
let query = use_query(Query::new(self.0, FetchUser));
match &*query.read().state() {
QueryStateData::Pending => "Loading...".to_string(),
QueryStateData::Loading { res } => {
format!("Refreshing... Previous: {:?}", res)
}
QueryStateData::Settled { res, .. } => {
format!("Result: {:?}", res)
}
}
}
}Multiple components using the same query (same capability type + same keys) share the same cache entry. The query only runs once and all subscribers receive the result.
§Reading query state
UseQuery gives access to the query state, see its docs for
the full API. The state is exposed as QueryStateData.
§Query configuration
Query supports builder methods to control caching behavior.
See its docs for the full list of options (stale_time, clean_time, interval_time, enable).
let user = use_query(
Query::new(1, FetchUser)
.stale_time(Duration::from_secs(300))
.clean_time(Duration::from_secs(600))
.interval_time(Duration::from_secs(30))
.enable(true),
);§Invalidating queries
You can manually trigger a re-fetch from a UseQuery:
// Fire-and-forget (spawns a background task)
user.invalidate();
// Await the result
let reader = user.invalidate_async().await;For broader invalidation, use QueriesStorage:
// Re-run ALL FetchUser queries
QueriesStorage::<FetchUser>::invalidate_all().await;
// Re-run only FetchUser queries matching specific keys
QueriesStorage::<FetchUser>::invalidate_matching(user_id).await;invalidate_matching calls the matches() method on each
cached query. By default matches() returns true (all queries match). Override it for selective invalidation:
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchUser {
user_id: u32,
}
impl QueryCapability for FetchUser {
type Ok = String;
type Err = String;
type Keys = u32;
async fn run(&self, user_id: &Self::Keys) -> Result<Self::Ok, Self::Err> {
Ok(format!("User {user_id}"))
}
fn matches(&self, keys: &Self::Keys) -> bool {
// Only invalidate if the user_id matches
&self.user_id == keys
}
}§Standalone queries
To run a query outside of a component (e.g. from an async task), use
QueriesStorage::get() with a GetQuery:
let reader = QueriesStorage::<FetchUser>::get(
GetQuery::new(user_id, FetchUser)
.stale_time(Duration::from_secs(60))
).await;§Mutations
§Defining a mutation
Implement MutationCapability to define a write operation:
#[derive(Clone, PartialEq, Hash, Eq)]
struct UpdateUser {
user_id: u32,
}
impl MutationCapability for UpdateUser {
type Ok = ();
type Err = String;
/// (field_name, new_value)
type Keys = (String, String);
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
// Send update to the API
Ok(())
}
/// Called after `run()` completes. Use this to invalidate related queries.
async fn on_settled(&self, _keys: &Self::Keys, result: &Result<Self::Ok, Self::Err>) {
if result.is_ok() {
QueriesStorage::<FetchUser>::invalidate_matching(self.user_id).await;
}
}
}The on_settled callback is the primary mechanism for
keeping query data consistent after mutations. It runs after run() regardless of success or failure.
§Using a mutation in a component
Call use_mutation with a Mutation:
#[derive(PartialEq)]
struct UserEditor(u32);
impl Component for UserEditor {
fn render(&self) -> impl IntoElement {
let mutation = use_mutation(Mutation::new(UpdateUser { user_id: self.0 }));
let status = match &*mutation.read().state() {
MutationStateData::Pending => "Ready",
MutationStateData::Loading { .. } => "Saving...",
MutationStateData::Settled { res, .. } if res.is_ok() => "Saved!",
MutationStateData::Settled { .. } => "Error",
};
rect().child(status).child(
Button::new()
.on_press(move |_| mutation.mutate(("name".to_string(), "Alice".to_string())))
.child("Save"),
)
}
}See UseMutation docs for the full API. The state is exposed
as MutationStateData.
Mutation also supports builder methods, see its docs.
§Captured values
Query and mutation types must implement PartialEq and Hash since they are used as cache keys.
This is a problem for values like API clients or State<T> handles that should not affect cache identity.
Captured<T> wraps a value so it is invisible to caching:
its PartialEq always returns true and its Hash is a no-op.
#[derive(Clone, PartialEq, Hash, Eq)]
struct FetchTodos(Captured<State<DbClient>>);
impl QueryCapability for FetchTodos {
type Ok = Vec<String>;
type Err = String;
type Keys = ();
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let _client: &State<DbClient> = &self.0;
Ok(vec![])
}
}Captured<T> implements Deref<Target = T> and DerefMut, so you can use the inner
value transparently.
§Examples
state_query.rs- Basic query usagestate_mutation.rs- Query + mutation with invalidationhackernews.rs- Fetching from a real API with stale timesstate_query_sqlite/- Full CRUD app with SQLite