Skip to main content

Crate freya_query

Crate freya_query 

Source
Expand description

§Freya Query

A powerful, async-focused data management library for Freya applications, inspired by React Query. It handles caching, background refetching, deduplication, and automatic invalidation for async operations.

It is available under the query feature flag of the freya crate:

[dependencies]
freya = { version = "...", features = ["query"] }

Freya’s built-in async primitives (spawn, spawn_forever, use_future) are great for individual async operations, but they don’t share results between components, deduplicate concurrent calls, or invalidate stale data. Freya Query builds on top of those primitives and adds caching, background refetching, mutations, and invalidation, making it the right choice for fetching from an HTTP API, a database, or any other remote source.

§Overview

Freya Query 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.

§Key Features

  • Automatic Caching: Query results are cached and reused across components.
  • Background Refetching: Stale data is automatically refreshed in the background.
  • Invalidation: Mutations can invalidate related queries to keep data fresh.
  • Deduplication: Multiple identical queries are automatically deduplicated.
  • Error Handling: Built-in error states.
  • Reactive: Integrates seamlessly with Freya’s reactive state system.

§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;
    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 result = 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 result = 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

Modules§

captured
mutation
prelude
query