Skip to main content

Module remote_data

Module remote_data 

Source
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