Realtime Queries 

The example-app API supports realtime queries which instantly updates the frontend to reflect backend changes. In frontend code, you can mark a query as realtime by passing live: true to the React hook. Realtime queries will watch for any backend record creates, updates, or deletes, and then fetch new data and trigger a re-render when they happen.

Realtime query support is included out-of-the-box in your @gadget-client package, as well as in the @gadgetinc/react hooks package with no changes necessary to your backend implementation. Often termed as subscriptions or change events in different systems, live: true queries offer an efficient alternative to change polling or page refreshes. Gadget's realtime queries leverage a WebSocket-based, high-performance implementation of the GraphQL @live directive.

Using live: true 

To start re-rendering your frontend components when backend data changes, add the live: true option to your useFindMany or useFindOne hook call:

frontend/routes/todos/index.js
JavaScript
1export const TodosPage = () => {
2 const [{ data, fetching, error }] = useFindMany(api.todo, {
3 // turn on the live query mode
4 live: true,
5 });
6
7 if (fetching) return <div>Loading...</div>;
8 if (error) return <div>Oh no... {error.message}</div>;
9 return (
10 <ul>
11 {data.todos.map((todo) => (
12 <li>{todo.title}</li>
13 ))}
14 </ul>
15 );
16};

With the live: true option in place, each user's browser will subscribe to changes from the backend, fetch any changed data, and trigger a re-render of the component whenever a new todo is added, an on-screen todo is deleted, or an on-screen todo's fields are updated.

The live: true option can be combined with the other options you might pass useFindMany, like sort, filter, and select. For example, we can show a live view of the most recent unfinished todos:

frontend/routes/index.js
JavaScript
1export const DashboardPage = () => {
2 // get a live view of the most recent 5 unfinished todos
3 const [{ data, fetching, error }] = useFindMany(api.todo, {
4 live: true,
5 filter: {
6 finished: { equals: false },
7 sort: { createdAt: "Descending" },
8 first: 5,
9 },
10 });
11
12 if (fetching) return <div>Loading...</div>;
13 if (error) return <div>Oh no... {error.message}</div>;
14
15 return (
16 <div>
17 <h2>Quick todo view</h2>
18 <ul>
19 {data.todos.map((todo) => (
20 <li>{todo.title}</li>
21 ))}
22 </ul>
23 </div>
24 );
25};

Minimum package versions 

Realtime queries are supported by @gadgetinc/react version 0.14.11 and above, and for Shopify app developers, @gadgetinc/react-shopify-app-bridge version 0.13.3 and above.

Realtime queries also require you to use the latest version of your @gadget-client package, which you can update using these instructions.

Supported realtime hooks 

The live: true option can be passed to the following React hooks:

HookNotes
useFindManyWill rerender when any on-screen record changes, or when new records are added or removed from the currently on-screen page
useFindOneWill rerender when any field of the record changes, or when the record is deleted
useFindByWill rerender when any field of the returned record changes, or when the record is deleted
useFindFirstWill rerender when any field of the returned record changes, when the record is deleted, or when the first record matching the filters changes

Combining with the select option 

Realtime queries can provide an up-to-date view of deeply nested data by using the select option to specify which fields to render to. If you use the select field to show data from related records by selecting through belongs to or has many fields, the related records will also be watched, and your component will re-render when the related data changes as well.

For example, if we have a todo model with a belongs to relationship to the user model capturing the assignedUser, we can show a view of realtime data of both the todo and the user's name with live: true and select:

frontend/routes/todos/index.js
JavaScript
1export const TodosPage = () => {
2 const [{ data, fetching, error }] = useFindMany(api.todo, {
3 live: true,
4 select: {
5 id: true,
6 title: true,
7 assignedUser: {
8 name: true,
9 },
10 },
11 });
12
13 if (fetching) return <div>Loading...</div>;
14 if (error) return <div>Oh no... {error.message}</div>;
15 return (
16 <ul>
17 {data.todos.map((todo) => (
18 <li>
19 {todo.title} {todo.assignedUser.name}
20 </li>
21 ))}
22 </ul>
23 );
24};

The above React component will re-render when:

  • any of the on-screen todo records are changed
  • a new todo is added or an existing todo is removed
  • when the assigned user of any of the on-screen todo records is changed
  • when the name of any of the assigned user records is updated

Combining with other options 

Realtime queries support all the same options as the non-live hook calls, like filter, sort, search, first, before, last and after. If these options are passed, a live: true hook will provide an up-to-date view of that filtered and sorted page of data. The component will re-render when data is removed from the current page, or when data on the page changes, but not when data on other pages changes.

For example, we can query for live data with a sort, a filter, and a max page size of 5:

JavaScript
1const [{ data, fetching, error }] = useFindMany(api.todo, {
2 // use a live query
3 live: true,
4 // show only todos matching this filter
5 filter: {
6 finished: { equals: false },
7 },
8 // show only 5, ordered by most recently created
9 sort: { createdAt: "Descending" },
10 first: 5,
11});

The above React component will re-render when:

  • any of the on-screen todo records are changed
  • a new todo is added that is unfinished

The component won't re-render when:

  • the 6th most recent todo is changed (or any other less recent one)
  • when a finished todo is changed

Result value and hook specifics 

See the @gadgetinc/react reference docs for more details on the specifics of each live: true hook.

Limits 

  • live: true queries are limited to the same maximum page size as other queries (250 records max). You can still use the pagination options like first and after with live queries to show a live view of a specific page of data.
  • live: true queries only run in environments with WebSocket support, which includes modern browsers, Node.js and other web runtimes, but excludes sandboxed environments like Shopify Checkout Extensions. If you need realtime query support in these locked down environments, please get in touch with us on Discord.
  • live: true queries can fetch computed fields and display them, but they won't automatically re-render when the value of computed fields change. For a work around on this, see below
  • live: true queries will be pushed new data once every 100ms at most
  • live: true queries will fetch and re-render with the most up-to-date version of the data whenever any one record changes. This means your frontend will get the latest version of a record, but there is no guarantee your frontend will see each version of a record, as some updates may be coalesced into the same frontend update. To guarantee that a particular record state is reacted to, use code in the backend Actions for your models.

Working with computed fields 

Realtime queries do not automatically refresh when the values of on-screen computed fields change. Only changes to stored fields for the onscreen records will trigger updates and re-renders.

To work around this limitation, you can fetch data related to the computed fields you're showing that changes at the same time. Because that stored data will be observed, it will trigger refreshes when it changes, which will re-compute the value of your computed fields.

For example, if you have a user model and a computed field that counts how many post records exist for that user, you might render a live: true query that selects a page of users and the postCount field:

JavaScript
1// won't refresh when the count of posts changes, as no post data is selected
2const [{ data, fetching, error }] = useFindMany(api.user, {
3 live: true,
4 select: {
5 id: true,
6 postCount: true,
7 },
8});

But, if you also select some of the user's posts (which affect the postCount), the query will refresh when the posts change, and the postCount will be re-computed:

JavaScript
1// will refresh when the count of posts changes, because post data is selected
2const [{ data, fetching, error }] = useFindMany(api.user, {
3 live: true,
4 select: {
5 id: true,
6 postCount: true,
7 posts: {
8 edges: {
9 node: {
10 id: true,
11 },
12 },
13 },
14 },
15});

This data may not be necessary for rendering your UI, but it will cause the realtime query to observe the correct data and trigger refreshes of your computed field.

Billing 

Gadget developers are billed for the request time used during each refresh of a realtime query.

For example, if a realtime query starts and takes 75ms to fetch the initial data from the backend, and then refreshes 3 times over the next 5 minutes, and each refresh takes 50ms, the total request time billed would be (75ms + (3 * 50ms)) = 225ms.

Realtime queries are not billed request time for the open websocket that is listening for changes -- only for underlying query re-executions. In the example above, the 5 minutes the query was open would not count towards request time.

Backend implementation 

Gadget's auto-generated API implements robust live query support through scalable database changefeeds. Internally, Gadget tracks all changes made to any records by any actions or internal API calls, and publishes these changes to an internal event bus. When a browser starts a realtime query, a WebSocket is opened to Gadget's backend, and Gadget's servers begin watching for changes relevant to specific queries. When a relevant change occurs, the live query runs again in the database, and the difference in result is sent to the browser as a JSON diff patch.

Your backend application doesn't not need to emit change events -- any change made by your backend or frontend code is automatically tracked.

Gadget uses the @live GraphQL query directive and the @n1ru4l/graphql-live-query ecosystem of packages to implement this functionality on the client.