Skip to content
All essays
ArchitectureMarch 15, 202515 min

GraphQL Live Queries: Real-Time Data Subscriptions

Master GraphQL Live Queries for real-time data synchronization. Build reactive applications with automatic updates.

Ü
Ümit Uz
Mobile & Full Stack Developer

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.

typescript
// 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

typescript
// 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

bash
# Install required packages
npm install @n1ru4l/graphql-live-query @envelop/live-query
npm install graphql-yoga

Basic Server Configuration

typescript
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

typescript
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

typescript
// 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 updates

Conditional Live Queries

typescript
// 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

typescript
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

typescript
// Only invalidate specific fields
liveQueryStore.invalidate('Query.user', { id: '1' }, ['posts']);

// Or invalidate specific entity types
liveQueryStore.invalidate('Post', { id: '123' });

Debouncing Multiple Updates

typescript
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

typescript
// 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

typescript
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 document

Real-Time Dashboard

typescript
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 updates

E-commerce Inventory

typescript
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 updated

Testing Live Queries

Testing with GraphQL Playground

typescript
// Enable live queries in GraphQL Playground
const yoga = createYoga({
  schema,
  plugins: [
    useLiveQuery({
      liveQueryStore,
    }),
  ],
  // Enable live queries in playground
  graphqlEndpoint: '/graphql',
});

Unit Testing

typescript
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

typescript
// Good for: User presence, notifications, stock prices
// Bad for: Static content, rarely changing data

2. Implement Proper Error Handling

typescript
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

typescript
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

typescript
// 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

typescript
// 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 complexity

Migration Guide

From Subscriptions to Live Queries

typescript
// 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

typescript
// 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

typescript
// 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!

Enjoyed this essay? Share it.

Related essays

Next essay
Building AI-Powered Web Applications