Understanding Live Queries
Live Queries provide automatic, real-time data synchronization without the complexity of manual subscriptions. Unlike traditional subscriptions that require explicit event handling, Live Queries automatically track and update data whenever it changes on the server.
// Traditional Subscription
const subscription = gql`
subscription OnUserUpdated {
userUpdated {
id
name
email
}
}
`;
// Live Query - automatically updates!
const liveQuery = gql`
query GetUserLive {
user(id: "123") {
id
name
email
posts {
id
title
}
}
}
`;How Live Queries Work
The Polling vs. Push Model
Traditional Polling:
Client → Server: "Any updates?" (every 5 seconds)
Server → Client: "No"
Live Queries:
Client → Server: "Watch this query"
Server → Client: "Data changed! Here's update"Implementation Architecture
// Server-side Live Query implementation
import { LiveQueryStore } from '@n1ru4l/graphql-live-query';
import { useLiveQuery } from '@envelop/live-query';
// Create live query store
const liveQueryStore = new LiveQueryStore();
// Setup server
const server = new ApolloServer({
schema,
plugins: [
useLiveQuery({
liveQueryStore,
}),
],
});
// Execute live query
const result = await liveQueryStore.execute({
schema,
document: parse(query),
variableValues: variables,
});Setting Up GraphQL Live Queries
Installation and Setup
# Install required packages
npm install @n1ru4l/graphql-live-query @envelop/live-query
npm install graphql-yogaBasic Server Configuration
import { createServer } from 'node:http';
import { createYoga } from 'graphql-yoga';
import { useLiveQuery } from '@envelop/live-query';
import { LiveQueryStore } from '@n1ru4l/graphql-live-query';
// Create live query store
const liveQueryStore = new LiveQueryStore();
// Define schema
const typeDefs = gql`
type Query {
user(id: ID!): User
users: [User!]!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Mutation {
updateUser(id: ID!, name: String): User!
createPost(userId: ID!, title: String!, content: String!): Post!
}
`;
// In-memory data store
const users = new Map();
const posts = new Map();
// Seed initial data
users.set('1', { id: '1', name: 'John Doe', email: 'john@example.com' });
users.set('2', { id: '2', name: 'Jane Smith', email: 'jane@example.com' });
posts.set('1', {
id: '1',
title: 'First Post',
content: 'Hello World',
authorId: '1',
});
// Resolvers with live query support
const resolvers = {
Query: {
user: (_: unknown, { id }: { id: string }) => {
return users.get(id);
},
users: () => {
return Array.from(users.values());
},
},
User: {
posts: (user: User) => {
return Array.from(posts.values()).filter(
(post) => post.authorId === user.id
);
},
},
Mutation: {
updateUser: (
_: unknown,
{ id, name }: { id: string; name: string }
) => {
const user = users.get(id);
if (!user) throw new Error('User not found');
user.name = name;
users.set(id, user);
// Notify live query store of the change
liveQueryStore.invalidate('Query.user', { id });
return user;
},
createPost: (
_: unknown,
{ userId, title, content }: { userId: string; title: string; content: string }
) => {
const post = {
id: String(posts.size + 1),
title,
content,
authorId: userId,
};
posts.set(post.id, post);
// Invalidate queries that fetch posts
liveQueryStore.invalidate('Query.user', { id: userId });
return post;
},
},
};
// Create GraphQL Yoga server
const yoga = createYoga({
schema: createSchema({ typeDefs, resolvers }),
plugins: [
useLiveQuery({
liveQueryStore,
}),
],
});
// Start server
const server = createServer(yoga);
server.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
});Client-Side Implementation
Using GraphQL Live Queries with React
import { useLiveQuery } from '@graphql-live/react';
import { gql, useMutation } from '@apollo/client';
// Define live query
const GET_USER_LIVE = gql`
query GetUserLive($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
content
}
}
}
`;
// Define mutations
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
email
}
}
`;
const CREATE_POST = gql`
mutation CreatePost($userId: ID!, $title: String!, $content: String!) {
createPost(userId: $userId, title: $title, content: $content) {
id
title
content
}
}
`;
// Component using live query
function UserProfile({ userId }: { userId: string }) {
// Live query - automatically updates when data changes
const { data, loading, error } = useLiveQuery(GET_USER_LIVE, {
variables: { id: userId },
});
const [updateUser] = useMutation(UPDATE_USER);
const [createPost] = useMutation(CREATE_POST);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
const handleUpdateName = async () => {
await updateUser({
variables: {
id: userId,
name: 'Updated Name',
},
});
// Live query will automatically update!
};
const handleCreatePost = async () => {
await createPost({
variables: {
userId,
title: 'New Post',
content: 'Post content',
},
});
// Live query will automatically update!
};
return (
<div>
<h2>{data?.user?.name}</h2>
<p>{data?.user?.email}</p>
<h3>Posts</h3>
<ul>
{data?.user?.posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button onClick={handleUpdateName}>Update Name</button>
<button onClick={handleCreatePost}>Create Post</button>
</div>
);
}Advanced Live Query Patterns
Nested Live Queries
// Live query with nested relationships
const GET_TEAM_WITH_PROJECTS = gql`
query GetTeamWithProjects($teamId: ID!) {
team(id: $teamId) {
id
name
members {
id
name
assignedTasks {
id
title
status
}
}
projects {
id
name
tasks {
id
title
assignee {
id
name
}
}
}
}
}
`;
// Any change to team, members, projects, or tasks
// will trigger automatic updatesConditional Live Queries
// Enable live queries conditionally
const { data } = useLiveQuery(GET_USER, {
variables: { id: userId },
// Only enable live query when user is focused
enabled: isUserFocused,
// Polling fallback for clients without WebSocket
pollInterval: 5000,
});Live Query with Pagination
const GET_USERS_PAGINATED = gql`
query GetUsersPaginated($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
email
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function UserList() {
const { data, fetchMore } = useLiveQuery(GET_USERS_PAGINATED, {
variables: { first: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
after: data?.users?.pageInfo?.endCursor,
},
});
};
return (
<div>
{data?.users?.edges?.map((edge) => (
<div key={edge.node.id}>{edge.node.name}</div>
))}
{data?.users?.pageInfo?.hasNextPage && (
<button onClick={loadMore}>Load More</button>
)}
</div>
);
}Performance Optimization
Selective Invalidation
// Only invalidate specific fields
liveQueryStore.invalidate('Query.user', { id: '1' }, ['posts']);
// Or invalidate specific entity types
liveQueryStore.invalidate('Post', { id: '123' });Debouncing Multiple Updates
import { debounce } from 'lodash';
const debouncedInvalidate = debounce(
(id: string) => {
liveQueryStore.invalidate('Query.user', { id });
},
1000,
{ leading: true }
);
// Use debounced invalidation for rapid updates
function onUserUpdate(userId: string) {
debouncedInvalidate(userId);
}Connection Pool Management
// Configure connection limits
const yoga = createYoga({
schema,
plugins: [
useLiveQuery({
liveQueryStore,
// Maximum number of concurrent live queries
maxConcurrentLiveQueries: 100,
// Timeout for inactive connections
idleConnectionTimeout: 60000,
}),
],
});Real-World Use Cases
Collaborative Document Editing
const GET_DOCUMENT = gql`
query GetDocument($id: ID!) {
document(id: $id) {
id
title
content
collaborators {
id
name
isOnline
}
version
}
}
`;
// All collaborators see real-time updates
// when anyone edits the documentReal-Time Dashboard
const GET_DASHBOARD = gql`
query GetDashboard($userId: ID!) {
user(id: $userId) {
notifications {
id
message
read
}
tasks {
id
title
status
}
analytics {
views
clicks
conversions
}
}
}
`;
// Dashboard updates automatically when:
// - New notification arrives
// - Task status changes
// - Analytics data updatesE-commerce Inventory
const GET_PRODUCT = gql`
query GetProduct($id: ID!) {
product(id: $id) {
id
name
price
inventory {
quantity
lowStockThreshold
}
reviews {
id
rating
comment
}
}
}
`;
// Product page updates when:
// - Inventory changes
// - New review is added
// - Price is updatedTesting Live Queries
Testing with GraphQL Playground
// Enable live queries in GraphQL Playground
const yoga = createYoga({
schema,
plugins: [
useLiveQuery({
liveQueryStore,
}),
],
// Enable live queries in playground
graphqlEndpoint: '/graphql',
});Unit Testing
import { LiveQueryStore } from '@n1ru4l/graphql-live-query';
describe('Live Queries', () => {
it('should update when data changes', async () => {
const store = new LiveQueryStore();
const initialResult = await store.execute({
schema,
document: parse(GET_USER),
variableValues: { id: '1' },
});
expect(initialResult.data?.user?.name).toBe('John Doe');
// Update user
await updateUser('1', { name: 'Jane Doe' });
// Wait for live query to update
await new Promise((resolve) => setTimeout(resolve, 100));
const updatedResult = await store.execute({
schema,
document: parse(GET_USER),
variableValues: { id: '1' },
});
expect(updatedResult.data?.user?.name).toBe('Jane Doe');
});
});Best Practices
1. Use Live Queries for Frequently Changing Data
// Good for: User presence, notifications, stock prices
// Bad for: Static content, rarely changing data2. Implement Proper Error Handling
const { data, error } = useLiveQuery(GET_USER, {
variables: { id: userId },
onError: (error) => {
console.error('Live query error:', error);
// Fallback to polling or show error
},
});3. Monitor Connection Health
import { useLiveQuery } from '@graphql-live/react';
function UserComponent() {
const { connectionStatus } = useLiveQuery();
useEffect(() => {
if (connectionStatus === 'disconnected') {
// Show warning or switch to polling
}
}, [connectionStatus]);
}4. Implement Rate Limiting
// Prevent excessive invalidations
const rateLimiter = new Map();
function invalidateWithRateLimit(key: string) {
const lastInvalidation = rateLimiter.get(key);
const now = Date.now();
if (!lastInvalidation || now - lastInvalidation > 1000) {
liveQueryStore.invalidate(key);
rateLimiter.set(key, now);
}
}Comparison: Live Queries vs Subscriptions
// Live Queries
// ✓ Automatic updates
// ✓ No manual event handling
// ✗ Requires constant connection
// ✗ More server overhead
// Subscriptions
// ✓ Event-driven
// ✓ Efficient for infrequent updates
// ✗ Manual event management
// ✗ More client complexityMigration Guide
From Subscriptions to Live Queries
// Before: Using subscriptions
const USER_UPDATED = gql`
subscription OnUserUpdated($id: ID!) {
userUpdated(id: $id) {
id
name
email
}
}
`;
// After: Using live queries
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;Troubleshooting
Live Queries Not Updating
// Ensure you're calling invalidate after mutations
Mutation: {
updateUser: (_, { id, name }) => {
const user = updateUser(id, { name });
liveQueryStore.invalidate('Query.user', { id });
return user;
}
}Connection Dropping
// Implement reconnection logic
const yoga = createYoga({
schema,
plugins: [
useLiveQuery({
liveQueryStore,
// Enable automatic reconnection
enableReconnect: true,
reconnectInterval: 1000,
}),
],
});Conclusion
GraphQL Live Queries provide a powerful, declarative approach to real-time data synchronization. By automatically tracking and updating data changes, they eliminate much of the complexity associated with manual subscription management while providing a smoother user experience.
Key benefits:
- Automatic real-time updates
- Declarative syntax
- Reduced client complexity
- Better user experience
- Easier to maintain
Start implementing Live Queries today and build reactive applications that keep users engaged with up-to-the-moment data!