Get real-time updates from SQL Connect

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_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:

  # 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.

  • _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:

  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.

Design a Mobile Site
View Site in Mobile | Classic
Share by: