Skip to content

Subscriptions

Learn how to handle real-time data updates with subscriptions.

Basic Subscription

Subscriptions enable real-time communication between client and server for chat messages, notifications, live dashboards, and more.

Define a subscription like queries and mutations:

typescript
import { graphql } from '$mearie';
import { useSubscription } from '@mearie/react';

const ChatMessages = ({ chatId }: { chatId: string }) => {
  const { data, error } = useSubscription(
    graphql(`
      subscription MessageAddedSubscription($chatId: ID!) {
        messageAdded(chatId: $chatId) {
          id
          content
          author {
            id
            name
            avatar
          }
          createdAt
        }
      }
    `),
    { chatId },
  );

  return (
    <div>
      {data && <div>New message: {data.messageAdded.content}</div>}
      {error && <div>Error: {error.message}</div>}
    </div>
  );
};

Other Frameworks

See Vue, Svelte, or Solid for framework-specific examples.

Client Setup

Configure your client to support subscriptions.

Server-Sent Events (SSE)

Simple HTTP-based protocol. Recommended for most use cases.

typescript
import { createClient, httpExchange, subscriptionExchange } from '@mearie/react'; // or @mearie/vue, @mearie/svelte, @mearie/solid
import { createClient as createSSEClient } from 'graphql-sse';
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [
    httpExchange({ url: 'https://api.example.com/graphql' }),
    subscriptionExchange({
      client: createSSEClient({
        url: 'https://api.example.com/graphql',
      }),
    }),
  ],
});

TIP

Install the graphql-sse package to use Server-Sent Events:

sh
npm install graphql-sse

WebSocket

Alternative protocol with lower latency using GraphQL over WebSocket.

typescript
import { createClient, httpExchange, subscriptionExchange } from '@mearie/react'; // or @mearie/vue, @mearie/svelte, @mearie/solid
import { createClient as createWSClient } from 'graphql-ws';
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [
    httpExchange({ url: 'https://api.example.com/graphql' }),
    subscriptionExchange({
      client: createWSClient({
        url: 'wss://api.example.com/graphql',
      }),
    }),
  ],
});

TIP

Install the graphql-ws package to use WebSocket:

sh
npm install graphql-ws

Data Ownership

data holds the most recent event and nothing else. Each event replaces the previous one wholesale — subscriptions never merge events into an accumulated value. To build up history, collect it yourself in onData (see Accumulating Events).

Every event belongs to the variables it was requested with. When variables change, Mearie tears down the stream and opens a new one: data is released, loading returns to true until the first event arrives for the new variables, and the released event stays available as previousData — data from a previous set of variables, never an earlier event under the current ones. Under skip: true the release still happens, but no stream is opened and loading stays false.

tsx
const { data, previousData } = useSubscription(
  graphql(`
    subscription UserStatusSubscription($userId: ID!) {
      userStatus(userId: $userId) {
        online
      }
    }
  `),
  { userId },
);

// Keeps showing the last known status until the new stream emits
const status = data ?? previousData;

For subscriptions, loading: true means no event has arrived for the current variables yet, not that a request is in flight — a stream that stays quiet after opening keeps reporting loading: true.

onData and onError fire only on real emissions from the server. Releasing data on a variables change is not an emission, so it never invokes either callback.

Latest State Only

Display only the current state (e.g., online status):

typescript
const UserStatus = ({ userId }: { userId: string }) => {
  const { data, loading, error } = useSubscription(
    graphql(`
      subscription UserStatusSubscription($userId: ID!) {
        userStatus(userId: $userId) {
          online
          lastSeen
          typing
        }
      }
    `),
    { userId },
  );

  if (loading) return <div>Connecting...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <span className={data.userStatus.online ? 'online' : 'offline'}>
        {data.userStatus.online ? 'Online' : 'Offline'}
      </span>
      {data.userStatus.typing && <span>typing...</span>}
    </div>
  );
};

Accumulating Events

Collect events over time (e.g., chat messages):

typescript
import { useState, useCallback } from 'react';
import { useSubscription, type DataOf } from '@mearie/react';
import { graphql } from '$mearie';
import type { MessageAddedSubscription } from '$mearie';

const ChatMessages = ({ chatId }: { chatId: string }) => {
  const [messages, setMessages] = useState<Message[]>([]);

  const handleMessageAdded = useCallback((data: DataOf<MessageAddedSubscription>) => {
    setMessages((prev) => [...prev, data.messageAdded]);
  }, []);

  useSubscription(
    graphql(`
      subscription MessageAddedSubscription($chatId: ID!) {
        messageAdded(chatId: $chatId) {
          id
          content
          author {
            id
            name
          }
          createdAt
        }
      }
    `),
    { chatId },
    {
      onData: handleMessageAdded,
    },
  );

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>{message.content}</div>
      ))}
    </div>
  );
};

Combining with Queries

Load initial data with a query, then subscribe for updates:

typescript
import { useState, useCallback, useEffect } from 'react';
import { useQuery, useSubscription, type DataOf } from '@mearie/react';
import { graphql } from '$mearie';
import type { MessageAddedSubscription } from '$mearie';

const ChatRoom = ({ chatId }: { chatId: string }) => {
  const [messages, setMessages] = useState<Message[]>([]);

  // Load initial messages
  const { data, loading } = useQuery(
    graphql(`
      query GetMessagesQuery($chatId: ID!) {
        messages(chatId: $chatId) {
          id
          content
        }
      }
    `),
    { chatId },
  );

  useEffect(() => {
    if (data) {
      setMessages(data.messages);
    }
  }, [data]);

  const handleMessageAdded = useCallback((data: DataOf<MessageAddedSubscription>) => {
    setMessages((prev) => [...prev, data.messageAdded]);
  }, []);

  // Subscribe to new messages
  useSubscription(
    graphql(`
      subscription MessageAddedSubscription($chatId: ID!) {
        messageAdded(chatId: $chatId) {
          id
          content
        }
      }
    `),
    { chatId },
    {
      onData: handleMessageAdded,
    },
  );

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
    </div>
  );
};

Subscription Options

Control subscription behavior with options:

typescript
import { useCallback } from 'react';
import { useSubscription, type DataOf } from '@mearie/react';
import { graphql } from '$mearie';
import type { MessageAddedSubscription } from '$mearie';

const handleData = useCallback((data: DataOf<MessageAddedSubscription>) => {
  console.log('New message:', data.messageAdded);
}, []);

const handleError = useCallback((error: Error) => {
  console.error('Subscription error:', error);
}, []);

const { data, error, loading } = useSubscription(
  graphql(`
    subscription MessageAddedSubscription($chatId: ID!) {
      messageAdded(chatId: $chatId) {
        id
        content
      }
    }
  `),
  { chatId },
  {
    // Skip execution conditionally
    skip: !chatId,

    // Process each event
    onData: handleData,

    // Handle errors
    onError: handleError,
  },
);

Best Practices

  • Name subscriptions with Subscription suffix (e.g., MessageAddedSubscription)
  • Load initial data with queries first, use subscriptions only for updates
  • Handle connection errors gracefully

Next Steps