Realtime Queries 

The example-app API supports realtime queries that instantly update your frontend to reflect backend changes. In frontend code, you can mark a query as realtime by passing live: true. 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 API call or React hook. For example, we can pass live: true to useFindMany or useFindOne in React code:

web/routes/todos/index.jsx
React
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};
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:

web/routes/index.jsx
React
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: { finished: { equals: false }, sort: { createdAt: "Descending" }, first: 5 },
6 });
7
8 if (fetching) return <div>Loading...</div>;
9 if (error) return <div>Oh no... {error.message}</div>;
10
11 return (
12 <div>
13 <h2>Quick todo view</h2>
14 <ul>
15 {data.todos.map((todo) => (
16 <li>{todo.title}</li>
17 ))}
18 </ul>
19 </div>
20 );
21};
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: { finished: { equals: false }, sort: { createdAt: "Descending" }, first: 5 },
6 });
7
8 if (fetching) return <div>Loading...</div>;
9 if (error) return <div>Oh no... {error.message}</div>;
10
11 return (
12 <div>
13 <h2>Quick todo view</h2>
14 <ul>
15 {data.todos.map((todo) => (
16 <li>{todo.title}</li>
17 ))}
18 </ul>
19 </div>
20 );
21};

Plain API client live queries 

If you aren't using React and making API calls with the api object directly, you can also pass live: true to subscribe to realtime changes.

Calling a find function on the api object with live: true returns an AsyncIterator object that you can iterate with for await (const ... of), instead of just one object.

For example, you'd fetch a todo record with id 123 once by calling findOne:

JavaScript
const todo = await api.todo.findOne("123");
console.log("Todo today: " + todo.text);
const todo = await api.todo.findOne("123");
console.log("Todo today: " + todo.text);

You can then fetch a stream of changes to id 123 by calling findOne with live: true:

JavaScript
for await (const todo of api.todo.findOne("123", { live: true })) {
console.log("Todo changed:", todo.text);
}
for await (const todo of api.todo.findOne("123", { live: true })) {
console.log("Todo changed:", todo.text);
}

The live: true option is also supported for findMany, findBy, and findFirst calls in the imperative API client.

To find the first page of todo records and subscribe to changes in these records over time, call .findMany({ live: true }):

JavaScript
1for await (const todos of api.todo.findMany({ live: true, first: 10 })) {
2 // todos will be a whole page of 10 records, updated with any changes
3 for (const todo of todos) {
4 console.log("Todo changed:", todo.text);
5 }
6}
1for await (const todos of api.todo.findMany({ live: true, first: 10 })) {
2 // todos will be a whole page of 10 records, updated with any changes
3 for (const todo of todos) {
4 console.log("Todo changed:", todo.text);
5 }
6}

AsyncIterator for live queries 

Gadget's support for live queries in the imperative API client uses AsyncIterator to power the streaming functionality. AsyncIterator can be iterated using the JS-builtin for await (const .. of) syntax, or with helper libraries like repeater.

If you need to take an action every time your result changes, but also move on to execute more code, you can kick off your async iterator in an async function that you don't await:

JavaScript
1const streamChanges = async () => {
2 // update a DOM element as the data changes
3 for await (const todo of api.todo.findOne("123", { live: true })) {
4 document.getElementById("#todo").innerHTML = todo.text;
5 }
6};
7
8void streamChanges();
9
10// do more setup stuff
1const streamChanges = async () => {
2 // update a DOM element as the data changes
3 for await (const todo of api.todo.findOne("123", { live: true })) {
4 document.getElementById("#todo").innerHTML = todo.text;
5 }
6};
7
8void streamChanges();
9
10// do more setup stuff

Gadget uses AsyncIterator because it's the only JavaScript-native primitive that supports streaming changes over time, and doesn't require bloating your API client's bundle size with extra libraries you may not use. AsyncIterator is well supported by modern browsers.

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:

web/routes/todos/index.jsx
React
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};
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});
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});
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});
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.

Manually trigger a realtime query 

You can use the touch() function on a record to manually trigger a re-execution of a realtime query when no fields on that record have changed, for example, when you know a computed field has changed and you want to update the displayed value.

Example of manually triggering a realtime query
JavaScript
1export const run: ActionRun = async ({ api, params }) => {
2 // update the grade field on the student record
3 await api.student.update(params.studentId, {
4 grade: 90,
5 });
6
7 // get the class record that the student is in (class has many students)
8 const klass = await api.record.findById(params.classId);
9
10 // update the updatedAt field of the record to trigger the realtime query
11 // so the computed field for the class grade average is re-calculated
12 klass.touch();
13 await save(klass);
14};
1export const run: ActionRun = async ({ api, params }) => {
2 // update the grade field on the student record
3 await api.student.update(params.studentId, {
4 grade: 90,
5 });
6
7 // get the class record that the student is in (class has many students)
8 const klass = await api.record.findById(params.classId);
9
10 // update the updatedAt field of the record to trigger the realtime query
11 // so the computed field for the class grade average is re-calculated
12 klass.touch();
13 await save(klass);
14};

For more information on touch(), see the Record API docs.

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.

Was this page helpful?