Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/smogon/pokemon-showdown-client/llms.txt

Use this file to discover all available pages before exploring further.

PSModel<T> is the lightweight observable at the heart of Pokémon Showdown’s reactive architecture. Every major piece of client state — the connection, preferences, teams, and the global PS object itself — is either a PSModel or extends one. Rather than relying on a virtual-DOM diffing library to decide what has changed, PSModel gives you fine-grained subscriptions: you ask to be told when something updates, and you get called directly. This makes it easy to integrate with Preact components, plain event handlers, or any other code that needs to react to data changes.
Unlike React’s usual immutable-data paradigm, PS Models are mutable. Mutations happen first; then update() is called to notify all subscribers. Do not call update() before the mutation is complete.

PSSubscription<T>

Every call to subscribe or subscribeAndRun returns a PSSubscription<T>. Hold on to this object so you can unsubscribe later.

Properties

observable
PSModel<T> | PSStreamModel<T>
required
The model this subscription belongs to. Used internally by unsubscribe() to locate and remove the subscription from the model’s list.
listener
(value: T) => void
required
The callback function that was passed to subscribe or subscribeAndRun. Called every time the model’s update() method fires.

Methods

unsubscribe()
void
Removes this subscription from its parent observable. Safe to call multiple times — if the subscription has already been removed the call is a no-op.
const sub = someModel.subscribe(handler);
// … later …
sub.unsubscribe();

PSModel<T>

class PSModel<T = null>
The base observable class. Defaults to a no-value model (T = null) where update() is called without arguments to signal a state change. Pass a type argument to send a value along with each notification.

Properties

subscriptions
PSSubscription<T>[]
required
The live list of all active subscriptions for this model. Managed automatically by subscribe() and PSSubscription.unsubscribe(). Iterate over this array yourself only if you need to inspect or bulk-remove subscriptions.

Methods

subscribe(listener)
PSSubscription<T>
Registers a listener function and returns a PSSubscription handle. The listener is not called immediately — it will be called the next time update() fires.
const sub = myModel.subscribe((value) => {
  console.log('Model updated with', value);
});
subscribeAndRun(listener, value?)
PSSubscription<T>
Combines subscribe with an immediate first call to the listener. Useful for initialising UI state without duplicating the render logic.
// Initialise and keep in sync — no separate "first render" needed
const sub = PS.subscribeAndRun(() => {
  setConnected(PS.connection?.connected ?? false);
});
update()
void
No-value overload. Declared as update(this: PSModel): void — available on any PSModel instance, regardless of T. Notifies every subscriber by calling their listener with undefined (cast to T internally via value!). Use this when the interesting information is the mutation itself, not a specific value.
PS.update(); // triggers a re-render of all PS subscribers
update(value)
void
Value overload. Notifies every subscriber, passing value to each listener.
// Example: a typed model that streams room IDs
const roomChanges = new PSModel<string>();
roomChanges.update('battle-showdown-1234');

Usage Examples

import { PSModel } from './client-core';

// A no-value model — subscribers are notified of "something changed"
class ThemeModel extends PSModel {
  isDark = false;

  toggleDark() {
    this.isDark = !this.isDark;
    this.update(); // notify all subscribers
  }
}

const theme = new ThemeModel();

Preact Component Integration

The standard integration pattern for Preact (used throughout the PS client) is to subscribe in componentDidMount and unsubscribe in componentWillUnmount. This matches the component lifecycle and avoids memory leaks from stale subscriptions.
import { Component } from 'preact';
import { PS } from './client-main';
import type { PSSubscription } from './client-core';

class ConnectionBadge extends Component {
  subscription: PSSubscription<null> | null = null;

  componentDidMount() {
    // subscribeAndRun handles the first render automatically
    this.subscription = PS.subscribeAndRun(() => this.forceUpdate());
  }

  componentWillUnmount() {
    this.subscription?.unsubscribe();
  }

  render() {
    const connected = PS.connection?.connected ?? false;
    return <span class={connected ? 'online' : 'offline'} />;
  }
}
Forgetting to call unsubscribe() in componentWillUnmount is the most common source of memory leaks in PS panels. Each PSSubscription holds a reference back to the model, preventing garbage collection of both the component and the model’s subscription list entry.

How PSModel Fits in the Architecture

PSModel and PSStreamModel share almost identical APIs, but they differ in one important way: PSModel does not keep a backlog of past events. If you call update(value) before anyone has subscribed, that value is lost. Use PSStreamModel whenever events can arrive before listeners are ready — for example, server messages that arrive during page load.
PS itself extends PSModel (with no type argument, so T = null). Calling PS.update() is the standard way to trigger a re-render of all top-level Preact panels. Individual models like PS.prefs, PS.teams, and PS.user have their own subscription trees for more targeted updates.
For read-only observation of a model you don’t own, always use the PSSubscription handle returned by subscribe / subscribeAndRun. Never mutate model.subscriptions directly — doing so bypasses the deduplication logic and can cause listeners to fire multiple times per update.

Build docs developers (and LLMs) love