# Advanced: making your own class configurable

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

`client.config` configures the SDK's own objects, and only those. Its keys are fixed: `channel`, `thread`, `messageComposer`, `messagePaginator`, `messageOperations`, `client`, `liveLocationManager` and `searchController`. `InstanceConfigTree` and `InstanceSetupFunctionArgs` are type aliases, not interfaces, so module augmentation can't add a key, and `client.config.setConfig("voiceNotes", ...)` doesn't compile. The code the SDK uses to resolve and apply settings isn't exported either.

A class of your own is configured by your own code. This page shows how to give it the same shape as the SDK's configurable objects, so the code that reads it works the same way, and how to let the server narrow it with the helper `stream-chat` exports. To configure the SDK's own objects, see [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/).

The example is a `VoiceNotes` class that holds the settings for recording voice notes in a channel: whether they're offered, the longest recording, and the longest caption.

## The contract

Configurable objects in the SDK expose the same members, and code that reads settings relies on that shape. Every one has `configState` and `config`, and all but a channel also have `updateConfig()`. Give your class all three:

| Member                | Type                         | What it is                                          |
| --------------------- | ---------------------------- | --------------------------------------------------- |
| `configState`         | `StateStore<TConfig>`        | The settings in effect, as a store to subscribe to. |
| `config`              | `Readonly<TConfig>`          | The same settings, read once.                       |
| `updateConfig(patch)` | `(Partial<TConfig>) => void` | Changes the settings of this one object.            |

`config` must always be the whole answer: defaults, app-wide settings, constructor options, `updateConfig()` calls and the server's say, already combined. Code that reads it should never have to combine it with something else, such as a server flag, to know whether a feature is on. Getting this wrong is the most common mistake with configurable objects.

## Step 1: Define the configuration and its defaults

Name the type after the class, `<Class>Config`, as the SDK does with `MessageComposerConfig` and `ChannelConfig`:

```ts title="voice-notes-config.ts"
export type VoiceNotesConfig = {
  /** Whether voice notes are offered. The channel type's `uploads` setting can turn them off. */
  enabled: boolean;
  /** The longest recording, in milliseconds. */
  maxDurationMs: number;
  /** The longest caption. The channel type's `max_message_length` caps it. */
  maxCaptionLength?: number;
};

export const DEFAULT_VOICE_NOTES_CONFIG: Readonly<VoiceNotesConfig> =
  Object.freeze({
    enabled: true,
    maxDurationMs: 2 * 60 * 1000,
  });
```

Three rules apply to defaults:

- **Freeze them.** Every resolved configuration is spread from the defaults, so a part no layer sets is the defaults object itself, reachable through `config`. A write through it would change the default for every instance. Frozen, the write fails instead. `Object.freeze` is enough for a flat configuration; freeze nested groups too.
- **Default a flag the server also controls to `true`.** The two are combined with AND, and `true` means "no opinion, let the server decide". A `false` default silently turns off a feature the server allows, and nobody suspects a second switch. Use `false` only for a feature the server has no say in, which is off until the app turns it on.
- **Prefer required booleans with a default over optional ones.** An optional flag's "off" is easy to confuse with "not set".

## Step 2: Keep app-wide settings in one place

`client.config` holds the settings for every instance of a kind. Do the same for your class with a store of your own:

```ts title="voice-notes-settings.ts"
import { StateStore } from "@stream-io/state-store";
import type { VoiceNotesConfig } from "./voice-notes-config";

/** Settings for every VoiceNotes instance, the existing ones and the ones created later. */
export const voiceNotesSettings = new StateStore<Partial<VoiceNotesConfig>>({});

export const setVoiceNotesSettings = (settings: Partial<VoiceNotesConfig>) => {
  voiceNotesSettings.next({
    ...voiceNotesSettings.getLatestValue(),
    ...settings,
  });
};
```

`StateStore` comes from `@stream-io/state-store`, the package `stream-chat` keeps its own stores in. Add it to your app's dependencies to import it, in the range `stream-chat` asks for (`^1.1.6`), so the app installs one copy.

An app then sets the settings once, wherever it creates the client: `setVoiceNotesSettings({ maxDurationMs: 60_000 })`.

## Step 3: Resolve the settings in a store

The class builds its settings from layers, the same way the SDK's objects do. A later layer wins for the fields it sets:

1. the defaults;
2. the app-wide settings;
3. the options passed to the constructor;
4. `updateConfig()` calls;
5. the server's say, last, so nothing above can go beyond it.

```ts title="voice-notes.ts"
import { StateStore } from "@stream-io/state-store";
import { mergeServerRestrictions } from "stream-chat";
import type {
  Channel,
  ServerRestrictions,
  ServerUpperBounds,
} from "stream-chat";
import {
  DEFAULT_VOICE_NOTES_CONFIG,
  type VoiceNotesConfig,
} from "./voice-notes-config";
import { voiceNotesSettings } from "./voice-notes-settings";

const isSameConfig = (a: VoiceNotesConfig, b: VoiceNotesConfig) =>
  a.enabled === b.enabled &&
  a.maxDurationMs === b.maxDurationMs &&
  a.maxCaptionLength === b.maxCaptionLength;

export class VoiceNotes {
  readonly configState: StateStore<VoiceNotesConfig>;
  private patches: Partial<VoiceNotesConfig> = {};
  private readonly unsubscribers: Array<() => void>;

  constructor(
    private readonly channel: Channel,
    private readonly options: Partial<VoiceNotesConfig> = {},
  ) {
    this.configState = new StateStore<VoiceNotesConfig>(this.resolve());
    this.unsubscribers = [
      // The app-wide settings changed.
      voiceNotesSettings.subscribe(() => this.rederive()),
      // The channel's first server response brings the channel type's settings.
      channel.state.subscribeWithSelector(
        ({ initialized }) => ({ initialized }),
        () => this.rederive(),
      ),
    ];
  }

  get config(): Readonly<VoiceNotesConfig> {
    return this.configState.getLatestValue();
  }

  updateConfig(patch: Partial<VoiceNotesConfig>) {
    this.patches = { ...this.patches, ...patch };
    this.rederive();
  }

  dispose() {
    this.unsubscribers.forEach((unsubscribe) => unsubscribe());
  }

  /** The server's switches. Read on every derivation, so a change is picked up. */
  private get serverRestrictions(): ServerRestrictions<VoiceNotesConfig> {
    return { enabled: this.channel.serverConfig?.uploads };
  }

  /** The server's limits. */
  private get serverUpperBounds(): ServerUpperBounds<VoiceNotesConfig> {
    return { maxCaptionLength: this.channel.serverConfig?.max_message_length };
  }

  private resolve(): VoiceNotesConfig {
    const requested: VoiceNotesConfig = {
      ...DEFAULT_VOICE_NOTES_CONFIG,
      ...voiceNotesSettings.getLatestValue(),
      ...this.options,
      ...this.patches,
    };

    return mergeServerRestrictions(
      requested,
      this.serverRestrictions,
      this.serverUpperBounds,
    );
  }

  private rederive() {
    const next = this.resolve();
    if (!isSameConfig(next, this.config)) this.configState.next(next);
  }
}
```

A few choices in it matter:

- **Every change rebuilds from all the layers.** `VoiceNotes.updateConfig()` records the request and rebuilds, and so does a change of the app-wide settings or of the server's answer. Writing a patch straight into the store instead would be lost at the next rebuild, or would outlive a server "no".
- **A rebuild that changes nothing publishes nothing.** Each rebuild makes a new object, so the store would notify its subscribers every time without the comparison.
- **`configState` is the only copy.** `config` reads it, so the two can't drift apart.
- **`VoiceNotes.dispose()` releases the subscriptions.** Without it, the app-wide store keeps every instance alive.

## Step 4: Let the server narrow it

Do this only if a server-side setting has a say in your feature. `mergeServerRestrictions(requested, restrictions, upperBounds?)`, exported from `stream-chat`, applies the server's say under the rules the SDK's own objects follow:

```ts
import { mergeServerRestrictions } from "stream-chat";

const resolved = mergeServerRestrictions(
  { enabled: true, maxCaptionLength: 5000, maxDurationMs: 60_000 },
  { enabled: false },
  { maxCaptionLength: 2000 },
);
// { enabled: false, maxCaptionLength: 2000, maxDurationMs: 60_000 }
```

- **Booleans are ANDed.** Either side can turn a feature off, and neither can turn it on alone. A switch the client turned off stays off where the server allows it, and the other way round.
- **`undefined` means "the server says nothing".** The request stands. This is how you say that the server has no say, for example when the files go to your own storage, so Stream's `uploads` setting doesn't apply. It's also the value of every server setting before the channel has been queried.
- **Limits go in `upperBounds`.** A restriction replaces the requested value, which for a limit would widen a stricter request. An upper bound keeps the smaller of the two, and applies alone when nothing was requested. `ServerRestrictions<TConfig>` and `ServerUpperBounds<TConfig>` type the two arguments.

The channel type's settings arrive with the channel, in `channel.serverConfig`, which is `undefined` until the channel has been queried or watched. An instance built before that holds the defaults, so it has to rebuild once the answer is there. `VoiceNotes` rebuilds when `channel.state` reports `initialized`, which the first server response sets after storing the settings. [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/#server-settings) lists the settings the server sends.

## Rules that are easy to get wrong

**Never read the raw server setting outside the restriction getters.** Once a switch is combined, `channel.serverConfig?.uploads` is only the server's half of the answer. Everything else reads `config`. Offering a feature that the client had already turned off is the bug this prevents.

**Don't guard setters on the value in effect.**

```ts
// Wrong: `this.config.enabled` already includes the server's say
set enabled(next: boolean) {
  if (next === this.config.enabled) return;
  this.updateConfig({ enabled: next });
}
```

Where the server turned the feature off, the value in effect never moves. The guard then skips recording a request, and an older request comes back the moment the server allows the feature again. Drop the guard: the rebuild already skips publishing when nothing changed.

**Re-apply values that are read once.** A setting that feeds something built once, such as a timer interval or a debounce, doesn't change by itself when the setting does. Subscribe to `configState` and rebuild that part, or document the setting as read at construction.

**Name the server side `serverConfig`, never `config`.** `channel.config` holds the settings in effect, and `channel.serverConfig` the channel type's server settings. Keep the same split in your own classes.

## Tests worth writing

- **The defaults are frozen.** A write to a field of the defaults throws in strict mode.
- **The layers win in order.** The app-wide settings beat the defaults, a constructor option beats the app-wide settings, and `updateConfig()` beats the constructor.
- **A write that changes nothing publishes nothing.** Subscribe, call `updateConfig()` with the current value, and check that the subscriber didn't run again.
- **Every client and server combination.** For each switch the server controls, check every pair of requested `true` or `false` with a server `true`, `false` or `undefined`. The `undefined` server case is the one that catches "no opinion" being treated as "no".
- **Each limit.** A larger request gets the server's limit, a smaller one keeps its own, and no request gets the server's.
- **A late server answer rebuilds the settings.** Create the instance before the channel is queried, then query it.

When you fix a bug in this area, undo the fix once and check that the new test fails.

## Checklist

- [ ] A `<Class>Config` type and frozen defaults.
- [ ] Switches the server also controls default to `true`; booleans are required, not optional.
- [ ] `configState`, `config` and `updateConfig()`, with `configState` as the only copy.
- [ ] App-wide settings in one store, and every instance rebuilds when they change.
- [ ] Every change rebuilds from all the layers, and a rebuild that changes nothing publishes nothing.
- [ ] `mergeServerRestrictions()` if the server has a say, with limits as upper bounds, and a rebuild once the channel's settings arrive.
- [ ] No reads of `channel.serverConfig` outside the restriction getters.
- [ ] No setters guarded on the value in effect.
- [ ] A `dispose()` that releases every subscription.
- [ ] The tests above.

## Classes inside stream-chat

A configurable class that ships in `stream-chat` itself is wired with the package's internal machinery, which isn't exported: a `ConfigController` resolves its layers and applies the server's say, `applyInstanceConfiguration` subscribes each instance to its key, and the key is added to `InstanceConfigTree` and `INSTANCE_CONFIG_TREE_SHAPE`. That work happens in the `stream-chat` repository, whose [instance configuration guide](https://github.com/GetStream/stream-chat-js/blob/release-v10/docs/instance-configuration.md) describes it.

## See also

- [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/): `client.config`, setup functions, the resolution order and the server settings.
- [Configuration tree](https://getstream.io/chat/docs/sdk/react/v15/reference/state/configuration-tree/): every setting of the SDK's own objects.
- [Reactive state](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/reactive-state/): stores and subscriptions.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/reference/state/making-your-own-class-configurable/](https://getstream.io/chat/docs/sdk/react/v15/reference/state/making-your-own-class-configurable/).