Your client code can subscribe to queries to get real-time updates when the
result of the query changes.

## Before you begin

- Set up SDK generation for your project as described in the documentation for
  [Apple platforms](https://firebase.google.com/docs/sql-connect/ios-sdk.md.txt), [Android](https://firebase.google.com/docs/sql-connect/android-sdk.md.txt), [web](https://firebase.google.com/docs/sql-connect/web-sdk.md.txt), and
  [Flutter](https://firebase.google.com/docs/sql-connect/flutter-sdk.md.txt).

  - You must enable client-side caching for all of your generated SDKs. Specifically, every SDK configuration must contain a declaration like the following:

  ```yaml
  clientCache:
    maxAge: 5s
    storage: ... # Optional.
  ```
- Your app clients must be must be using a recent version of the
  SQL Connect core SDK:

  - Apple: Firebase SQL Connect SDK for Swift version 11.12.0 or newer
  - Android: Firebase SQL Connect SDK version 17.3.0 or newer (BoM version 34.14.0 or newer)
  - Web: JavaScript SDK version 12.12.0 or newer
  - Flutter: `firebase_data_connect` version 0.3.0 or newer
- Regenerate your client SDKs using version 15.14.0 of the Firebase CLI, or
  newer.

## Subscribing to query results

You can subscribe to a query to respond to changes in the query result. For
example, suppose you have the following schema and operations defined in your
project:

```graphql
# dataconnect/schema/schema.gql

type Movie @table(key: "id") {
  id: UUID! @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String
  description: String
  averageRating: Int
}
```

```graphql
# dataconnect/connector/operations.gql

query GetMovieById($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    id
    title
    releaseYear
    genre
    description
  }
}

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      description: $description
    })
}
```

To subscribe to changes in the result of running `GetMovieById`:

### Web

```typescript
import { subscribe, DataConnectError, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';

const queryRef = getMovieByIdRef({ id: "<MOVIE_ID>" });

// Called when receiving an update.
const onNext = (result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>) => {
  console.log("Movie <MOVIE_ID> updated", result);
}

const onError = (err?: DataConnectError) => {
  console.error("received error", err);
}

// Called when unsubscribing or when the subscription is automatically released.
const onComplete = () => {
  console.log("subscription complete!");
}

const unsubscribe = subscribe(queryRef, onNext, onError, onComplete);
```

### Web (React)

```tsx
import { subscribe, QueryResult } from 'firebase/data-connect';
import { getMovieByIdRef, GetMovieByIdData, GetMovieByIdVariables } from '@dataconnect/generated';
import { useState, useEffect } from "react";

export const MovieInfo = ({ id: movieId }: { id: string }) => {
  const [movieInfo, setMovieInfo] = useState<GetMovieByIdData>();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const queryRef = getMovieByIdRef({ id: movieId });

    function updateUi(result: QueryResult<GetMovieByIdData, GetMovieByIdVariables>): void {
      setMovieInfo(result.data);
      setLoading(false);
    }

    const unsubscribe = subscribe(
      queryRef,
      updateUi,
      (err) => {
        setError(err ?? new Error("Unknown error occurred"));
        setLoading(false);
      }
    );

    return () => unsubscribe();
  }, [movieId]);

  if (loading)
    return <div>Loading movie details...</div>;
  if (error || !movieInfo || !movieInfo.movie)
    return <div>Error loading movie details: {error?.message}</div>;

  return (
    <div>
      <h2>{movieInfo.movie.title} ({movieInfo.movie.releaseYear})</h2>
      <ul>
        <li>Genre: {movieInfo.movie.genre}</li>
        <li>Description: {movieInfo.movie.description}</li>
      </ul>
    </div>
  );
};
```

SQL Connect also supports caching and real-time subscriptions using
TanStack. When you specify `react: true` or `angular: true` in your
`connector.yaml` file, SQL Connect generates
bindings for React or Angular using TanStack.

These bindings can work alongside of
SQL Connect's built-in real-time support, but only with some
difficulty. We recommend that you use either the TanStack-based bindings or
SQL Connect's built-in real time support, but **not both**.

Note that SQL Connect's own real-time implementation has some
advantages over the TanStack bindings:

- Normalized caching: SQL Connect implements *normalized caching*, which improves data consistency as well as memory and network efficiency compared to query-level caching. With normalized caching, if an entity updates in one area of your app, it will also update in other areas that use that entity.
- Remote invalidation: SQL Connect can remotely invalidate cached entities on all subscribed devices.

If you choose not to use TanStack, you should remove the `react: true` and
`angular: true` settings from your `connector.yaml` file.

### iOS

```swift
struct MovieDetailsView: View {
    // QueryRef has the @Observable annotation, so its properties will
    // automatically trigger updates on changes.
    // Realtime subscriptions will keep the query results updated with changes.

    // Define the ref variable.
    // If parameters are known before hand, refs can be initialized here directly
    // else they can be initialized in the init for the view, like here
    @State private var queryRef: GetMovieByIdQuery.Ref

    // Store the handle to unsubscribe from query updates.
    // QueryRef can be used in multiple views.
    // Each view can separately subscribe / unsubscribe to updates
    // When there are no more subscribers to a QueryRef,
    // it will cancel automatic updates for that QueryRef.
    @State private var querySub: AnyCancellable?

    init(movieId: String) {
      // initialize the ref with the movieId
      queryRef = DataConnect.moviesConnector.getMovieByIdQuery.ref(movieId: movieId)
    }

    var body: some View {
        VStack {
            // Use the query results in a View.
            if let movie = queryRef.data?.movie {
              Text(movie.title)
              Text(mpvie.description)
              // other details
            } else {
              // if last fetch/update resulted in an error
              if error = queryRef.lastError {
                Text("Error loading movie")
              } else {
                Text("Loading movie ...")
              }
            }
        }
        .onAppear {
            // Subscribe to the query for updates using the Observable macro.
            Task {
                do {
                    querySub = try await queryRef.subscribe().sink { _ in }
                } catch {
                    print("Error subscribing to query: \(error)")
                }
            }
        }
        .onDisappear {
          // Calling cancel will unsubscribe from receiving updates.
          querySub?.cancel()
        }
    }
}
```

### Android

```kotlin
class ExampleViewModel(
  private val movieId: UUID
) : ViewModel() {
  private val _uiState = MutableStateFlow<GetMovieByIdQuery.Data.Movie?>(null)
  val uiState = _uiState.asStateFlow()

  // Subscribe to the query.
  private val movieInfoSub = ExampleConnector.instance
      .getMovieById.ref(GetMovieByIdQuery.Variables(id = movieId))
      .subscribe()

  init {
    viewModelScope.launch {
      movieInfoSub.flow.collect {
        // As query results are collected, update the UI state.
        val result = it.result.getOrElse { return@collect }
        _uiState.update{ result.data.movie }
      }
    }
  }

  companion object {
    fun provideFactory(movieId: UUID): ViewModelProvider.Factory =
      viewModelFactory {
        initializer {
          ExampleViewModel(movieId = movieId)
        }
      }
  }
}
```

### Flutter

Import your project's generated SDK:

```dart
import 'package:flutter_app/dataconnect_generated/generated.dart';
```

Then call the `subscribe()` method on a query reference:

```dart
final queryRef = MovieConnector.instance.getMovieById(id: "<MOVIE_ID>").ref();
final subscription = queryRef.subscribe().listen((result) {
  final movie = result.data.movie;
  if (movie != null) {
    // Execute your logic to update the UI with the refreshed movie information.
    updateUi(movie.title);
  }
});
```

To stop updates, you can call `subscription.cancel()`.

Once you are subscribed to the query as in the preceding example, you will get
updates whenever the result of the specific query changes. For example, if
another client executes the `UpdateMovie` mutation on the same ID you subscribed
to, you will get an update.

## Implicit query refresh signals

In the preceding example, you were able to subscribe to a query and get
real-time updates without any additional modifications to your operations. In
particular, you didn't need to specify that the `UpdateMovie` mutation can
affect the result of the `GetMovieById` query.

This is possible because the `GetMovieById` query implicitly gets a refresh
signal from the `UpdateMovie` mutation. Implicit refresh signals are sent
between a subset of the queries and mutations that you might write:

If your **query** performs a **single entity lookup by primary key** , then any
**mutation** that writes to the same entity, **also identified by its primary
key** will implicitly trigger a refresh signal.

- `_insert` and `_insertMany`
- `_upsert` and `_upsertMany`
- `_update`
- `_delete`

`_deleteMany` and `_updateMany` don't send refresh signals.

In the prior example, the `GetMovieById` query looks up a single movie by ID
(`movie(id: $id)`) and the `UpdateMovie` mutation updates a single movie,
specified by ID (`movie_update(id: $id, ...)`), so the query can take advantage
of implicit refresh.

Insert and upsert operations can trigger implicit refresh signals when you are
keying off a known value, such as a Firebase Authentication user's UID.

For example, consider a query like the following:

```graphql
query GetExtendedProfileByUser @auth(level: USER) {
  profile(key: { id_expr: "auth.uid" }) {
    id
    status
    photoUrl
    socialLink
  }
}
```

The query would implicitly receive a refresh signal from a mutation like the
following:

```graphql
mutation UpsertExtendedProfile($status: String, $photoUrl: String, $socialLink: String) @auth(level: USER) {
  profile_upsert(
    data: {
      id_expr: "auth.uid"
      status: $status
      photoUrl: $photoUrl
      socialLink: $socialLink
    }
  ) {
    id
    status
    photoUrl
    socialLink
  }
}
```

When your queries or mutations are more complicated, you will need to
specify the conditions that require a query refresh.
Continue to the next section to learn how.

## Explicit query refresh signals

In addition to the refresh signals that are implicitly sent by mutations to
queries, you can also explicitly specify when a query should receive a refresh
signal. You do this by annotating your queries with the `@refresh` directive.

Using the `@refresh` directive is required whenever your queries don't meet the
specific criteria (see above) for automatic automatic refresh. Some examples of
queries that must include this directive include:

- Queries that retrieve **lists of entities**
- Queries that perform **joins on other tables**
- **Aggregation** queries
- Queries using **native SQL**
- Queries using **custom resolvers**

You can specify a refresh policy in two ways:

### Time-based intervals

Refresh the query on a fixed time interval.

For example, suppose your very active user base can result in a movie's
cumulative rating being updated many times every minute, particularly after a
movie's release. Rather than refreshing the query every time the rating changes,
you could instead refresh the query every few seconds, to get updates reflecting
the cumulative result of potentially several mutations.

```graphql
# dataconnect/connector/operations.gql

query GetMovieRating($id: UUID!) @auth(level: PUBLIC) @refresh(every: {seconds: 30}) {
  movie(id: $id) {
    id
    averageRating
  }
}
```

### Mutation execution

Refresh the query when a specific mutation is executed. This approach makes
explicit which mutations have the potential to change the result of the query.

For example, suppose you have a query that retrieves information about multiple
movies instead of a specific one. This query should refresh whenever a mutation
has updated any of the movie records.

```graphql
query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(onMutationExecuted: { operation: "UpdateMovie" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}
```

You can also specify a CEL expression condition that must be satisfied for the
mutation to trigger a query refresh.

Doing so is highly recommended. The more precise you can be when specifying
the condition, the fewer unnecessary database resources will be consumed, and
the more responsive your app will be.

For example, suppose you had a query that
listed movies only in a specified genre. This query should only refresh when a
mutation updates a movie in the same genre:

```graphql
query ListMoviesByGenre($genre: String, $offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list movies.")
    @refresh(onMutationExecuted: {
      operation: "UpdateMovie",
      condition: "request.variables.genre == mutation.variables.genre"
    }) {
  movies(
      where: { genre: { eq: $genre } },
      limit: 10,
      offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}
```

#### CEL bindings in @refresh conditions

The `condition` expression in `onMutationExecuted` has access to two contexts:

##### `request`

The state of the query being subscribed to.

| Binding | Description |
|---|---|
| `request.variables` | Variables passed to the query (for example, `request.variables.id`) |
| `request.auth.uid` | Firebase Authentication UID of the user who executed the query |
| `request.auth.token` | Dictionary of Firebase Authentication token claims for the user who executed the query |

##### `mutation`

The state of the mutation that executed.

| Binding | Description |
|---|---|
| `mutation.variables` | Variables passed to the mutation (e.g., `mutation.variables.movieId`) |
| `mutation.auth.uid` | Firebase Authentication UID of the user who executed the mutation |
| `mutation.auth.token` | Dictionary of Firebase Authentication token claims for the user who executed the mutation |

##### Common Patterns

    # Refresh only when the mutation targets the same entity
    "request.variables.id == mutation.variables.id"

    # Refresh only when the same user who subscribed makes a change
    "request.auth.uid == mutation.auth.uid"

    # Refresh when a specific field value matches a condition
    "request.auth.uid == mutation.auth.uid && mutation.variables.status == 'PUBLISHED'"

    # Refresh when a specific flag is set in the mutation
    "mutation.variables.isPublic == true"

### Multiple @refresh directives

You can specify the `@refresh` directive multiple times on a query to trigger a
refresh whenever any of the criteria specified by one of the `@refresh`
directives are satisfied.

For example, the following query will refresh every 30 seconds as well as
whenever one of the specified mutations are executed:

```graphql
query ListMovies($offset: Int)
    @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.")
    @refresh(every: {seconds: 30})
    @refresh(onMutationExecuted: { operation: "UpdateMovie" })
    @refresh(onMutationExecuted: { operation: "BulkUpdateMovies" }) {
  movies(limit: 10, offset: $offset) {
    id
    title
    releaseYear
    genre
    description
  }
}
```

### Reference

See the [`@refresh` directive reference](https://firebase.google.com/docs/reference/sql-connect/gql/directive.md.txt#refresh) for more examples.