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 web , Apple platforms , and Flutter .
- You must enable client-side caching for all of your generated SDKs. Specifically, every SDK configuration must contain a declaration like the following:
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
- Web: JavaScript SDK version 12.12.0 or newer
- Flutter:
firebase_data_connectversion 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:
# dataconnect/schema/schema.gql
type
Movie
@table(key:
"id")
{
id
:
UUID
!
@default
(
expr
:
"uuidV4()"
)
title
:
String
!
releaseYear
:
Int
genre
:
String
description
:
String
averageRating
:
Int
}
# 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
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)
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
struct
ListMovieView
:
View
{
// QueryRef has the Observable attribute, so its properties will
// automatically trigger updates on changes.
private
var
queryRef
=
connector
.
listMoviesByGenreQuery
.
ref
(
genre
:
"Sci-Fi"
)
// Store the handle to unsubscribe from query updates.
@
State
private
var
querySub
:
AnyCancellable
?
var
body
:
some
View
{
VStack
{
// Use the query results in a View.
ForEach
(
queryRef
.
data
?.
movies
??
[],
id
:
\
.
self
.
id
)
{
movie
in
Text
(
movie
.
title
)
}
}
.
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
{
querySub
?.
cancel
()
}
}
}
Flutter
Import your project's generated SDK:
import
'package:flutter_app/dataconnect_generated/generated.dart'
;
Then call the subscribe()
method on a query reference:
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 queryperforms a single entity lookup by primary key, then any mutationthat writes to the same entity, also identified by its primary keywill implicitly trigger a refresh signal.
-
_insertand_insertMany -
_upsertand_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:
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:
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
- Aggregationqueries
- 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.
# 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.
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:
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:
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
for more examples.

