Events enable real-time communication from actors to clients. While clients use actions to send data to actors, events allow actors to push updates to connected clients instantly.

Events work through persistent connections (WebSocket or SSE). Clients establish connections using .connect() and then listen for events with .on().

Publishing Events from Actors

Broadcasting to All Clients

Use c.broadcast(eventName, data) to send events to all connected clients:

import { actor } from "@rivetkit/actor";

const chatRoom = actor({
  state: { 
    messages: [] as Array<{id: string, userId: string, text: string, timestamp: number}>
  },
  
  actions: {
    sendMessage: (c, userId: string, text: string) => {
      const message = {
        id: crypto.randomUUID(),
        userId,
        text,
        timestamp: Date.now()
      };
      
      c.state.messages.push(message);
      
      // Broadcast to all connected clients
      c.broadcast('messageReceived', message);
      
      return message;
    },
    
    deleteMessage: (c, messageId: string) => {
      const messageIndex = c.state.messages.findIndex(m => m.id === messageId);
      if (messageIndex !== -1) {
        c.state.messages.splice(messageIndex, 1);
        
        // Notify all clients about deletion
        c.broadcast('messageDeleted', { messageId });
      }
    }
  }
});

Sending to Specific Connections

Send events to individual connections using conn.send(eventName, data):

import { actor } from "@rivetkit/actor";

const gameRoom = actor({
  state: { 
    players: {} as Record<string, {health: number, position: {x: number, y: number}}>
  },
  
  createConnState: (c, { params }) => ({
    playerId: params.playerId,
    role: params.role || "player"
  }),
  
  actions: {
    updatePlayerPosition: (c, position: {x: number, y: number}) => {
      const playerId = c.conn.state.playerId;
      
      if (c.state.players[playerId]) {
        c.state.players[playerId].position = position;
        
        // Send position update to all OTHER players
        for (const conn of c.conns) {
          if (conn.state.playerId !== playerId) {
            conn.send('playerMoved', { playerId, position });
          }
        }
      }
    },
    
    sendPrivateMessage: (c, targetPlayerId: string, message: string) => {
      // Find the target player's connection
      const targetConn = c.conns.find(conn => 
        conn.state.playerId === targetPlayerId
      );
      
      if (targetConn) {
        targetConn.send('privateMessage', {
          from: c.conn.state.playerId,
          message,
          timestamp: Date.now()
        });
      } else {
        throw new Error("Player not found or not connected");
      }
    }
  }
});

Event Filtering by Connection State

Filter events based on connection properties:

import { actor } from "@rivetkit/actor";

const newsRoom = actor({
  state: { 
    articles: [] as Array<{id: string, category: string, content: string, level: 'public' | 'premium'}>
  },
  
  createConnState: (c, { params }) => ({
    userId: params.userId,
    subscription: params.subscription || 'free' // 'free', 'premium'
  }),
  
  actions: {
    publishArticle: (c, article: {category: string, content: string, level: 'public' | 'premium'}) => {
      const newArticle = {
        id: crypto.randomUUID(),
        ...article,
        timestamp: Date.now()
      };
      
      c.state.articles.push(newArticle);
      
      // Send to appropriate subscribers only
      for (const conn of c.conns) {
        const canAccess = article.level === 'public' || 
                         conn.state.subscription === 'premium';
        
        if (canAccess) {
          conn.send('newArticle', newArticle);
        }
      }
      
      return newArticle;
    }
  }
});

Subscribing to Events from Clients

Clients must establish a connection to receive events from actors. Use .connect() to create a persistent connection, then listen for events.

Basic Event Subscription

Use connection.on(eventName, callback) to listen for events:

import { createClient } from "@rivetkit/actor/client";
import type { registry } from "./registry";

const client = createClient<typeof registry>("http://localhost:8080");

// Get actor handle and establish connection
const chatRoom = client.chatRoom.getOrCreate(["general"]);
const connection = chatRoom.connect();

// Listen for events
connection.on('messageReceived', (message) => {
  console.log(`${message.userId}: ${message.text}`);
  displayMessage(message);
});

connection.on('messageDeleted', ({ messageId }) => {
  console.log(`Message ${messageId} was deleted`);
  removeMessageFromUI(messageId);
});

// Call actions through the connection
await connection.sendMessage("user-123", "Hello everyone!");

One-time Event Listeners

Use connection.once(eventName, callback) for events that should only trigger once:

const gameRoom = client.gameRoom.getOrCreate(["room-456"]);
const connection = gameRoom.connect({
  playerId: "player-789",
  role: "player"
});

// Listen for game start (only once)
connection.once('gameStarted', () => {
  console.log('Game has started!');
  showGameInterface();
});

// Listen for game events continuously
connection.on('playerMoved', ({ playerId, position }) => {
  updatePlayerPosition(playerId, position);
});

connection.on('privateMessage', ({ from, message }) => {
  showPrivateMessage(from, message);
});

Removing Event Listeners

Use connection.off() to remove event listeners:

const messageHandler = (message) => {
  console.log("Received:", message);
};

// Add listener
connection.on('messageReceived', messageHandler);

// Remove specific listener
connection.off('messageReceived', messageHandler);

// Remove all listeners for an event
connection.off('messageReceived');

// Remove all listeners
connection.off();

React Integration

RivetKit’s React hooks provide a convenient way to handle events in React components:

import { useActor } from "./rivetkit";
import { useState } from "react";

function ChatRoom() {
  const [messages, setMessages] = useState([]);
  
  const chatRoom = useActor({
    name: "chatRoom",
    key: ["general"]
  });

  // Listen for new messages
  chatRoom.useEvent("messageReceived", (message) => {
    setMessages(prev => [...prev, message]);
  });

  // Listen for deleted messages
  chatRoom.useEvent("messageDeleted", ({ messageId }) => {
    setMessages(prev => prev.filter(m => m.id !== messageId));
  });

  const sendMessage = async (text: string) => {
    await chatRoom.connection?.sendMessage("user-123", text);
  };

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <strong>{message.userId}:</strong> {message.text}
        </div>
      ))}
      <MessageInput onSend={sendMessage} />
    </div>
  );
}

Connection Lifecycle Events

Connections emit lifecycle events you can listen to:

const connection = actor.connect();

connection.on('connected', () => {
  console.log('Connected to actor');
  enableUI();
});

connection.on('disconnected', () => {
  console.log('Lost connection to actor');
  showReconnectingIndicator();
});

connection.on('reconnected', () => {
  console.log('Reconnected to actor');
  hideReconnectingIndicator();
});

connection.on('error', (error) => {
  console.error('Connection error:', error);
  showErrorMessage(error.message);
});

Advanced Event Patterns

Event Buffering

Events are automatically buffered during disconnections and replayed on reconnection:

const connection = actor.connect();

// Events sent while disconnected are queued
connection.on('importantUpdate', (data) => {
  // This will still be called for events sent during disconnection
  // once the connection is reestablished
  handleImportantUpdate(data);
});

Connection Parameters

Pass parameters when connecting to provide context to the actor:

const gameRoom = client.gameRoom.getOrCreate(["competitive-room"]);
const connection = gameRoom.connect({
  playerId: getCurrentPlayerId(),
  skillLevel: getUserSkillLevel(),
  authToken: getAuthToken()
});

// The actor can use these parameters in its onBeforeConnect hook
// or access them via c.conn.params in actions

Conditional Event Handling

Handle events conditionally based on connection state:

connection.on('playerMoved', ({ playerId, position }) => {
  // Only update if it's not our own player
  if (playerId !== getCurrentPlayerId()) {
    updatePlayerPosition(playerId, position);
  }
});

connection.on('newArticle', (article) => {
  // Handle based on article level and user subscription
  if (article.level === 'premium' && !hasSubscription()) {
    showUpgradePrompt();
  } else {
    displayArticle(article);
  }
});

Error Handling

Handle event-related errors gracefully:

try {
  const connection = actor.connect();
  
  connection.on('messageReceived', (message) => {
    try {
      validateMessage(message);
      displayMessage(message);
    } catch (error) {
      console.error('Invalid message format:', error);
      // Handle malformed event data
    }
  });
  
} catch (error) {
  console.error('Failed to establish connection:', error);
  showConnectionError();
}

Best Practices

  1. Always use connections for events: Events only work through .connect(), not direct action calls
  2. Handle connection lifecycle: Listen for connection, disconnection, and error events
  3. Clean up listeners: Remove event listeners when components unmount
  4. Validate event data: Don’t assume event payloads are always correctly formatted
  5. Use React hooks: For React apps, use useActor and actor.useEvent for automatic cleanup
  6. Buffer critical events: Design actors to resend important events on reconnection if needed