Upgrade stream-chat to v9

List of Breaking Changes

Considered Major

  • removal of StreamChatGenerics (DefaultGenerics and ExtendableGenerics types too)
  • custom property name has been removed from type ChannelResponse, see note in Replacement - Module Augmentation section
  • removal of the jsDelivr (UMD) bundle

Considered Minor

  • type InviteOptions has been renamed to UpdateChannelOptions
  • type UpdateChannelOptions has been renamed to UpdateChannelTypeRequest
  • type ThreadResponseCustomData has been renamed to CustomThreadType
  • type MarkAllReadOptions has been deleted in favour of type MarkChannelsReadOptions
  • type QueryFilter no longer supports $ne and $nin operators
  • type ChannelMembership has been deleted in favour of type ChannelMemberResponse
  • function formatMessage (utils.ts) no longer returns __html property in the formatted message output

Note that $ne and $nin operators are inefficient and cause performance issues. You’ll need to refactor your queries removing all $ne or $nin operators. Feel free to contact our Customer Support to get help finding an appropriate alternative solution for your integration.

Removal of StreamChatGenerics

The generic was initially introduced to our codebase as a mean of providing our integrators with a way to extend our types with their own type definitions. This was mostly targeted at custom data of certain entities such as a message or a channel. Unfortunately cost of maintaining a codebase with such mechanic in place was quite high in comparison to the amount of benefits it brought to our integrators. It also fell short when it came to ease of use. Integrators would have to forward custom-defined type definitions to multiple places seemingly unrelated to the code at hand.

Main Reasons

There are two main issues with the generic mechanism.

Fragile

Integrators would have to define all of the custom types within the generic type even if they augmented only one or two specific entities which also meant that every time we issued a release with a new feature that extended the DefaultGenerics, just upgrading to the latest package version wouldn’t suffice, the generic type would have to be manually adjusted as well even if not explicitly needed.

types.ts
export type LocalStreamChatGenerics = {
  // the only augmentations I (as an integrator) currently care about
  attachmentType: { myCustomThing?: string };
  userType: { profilePicture?: string };
  // the rest which would have to be defined to be accepted by the SDK
  channelType: Record<string, unknown>;
  commandType: string;
  eventType: Record<string, unknown>;
  memberType: Record<string, unknown>;
  messageType: Record<string, unknown>;
  pollOptionType: Record<string, unknown>;
  pollType: Record<string, unknown>;
  reactionType: Record<string, unknown>;
  // list would grow with each update to the `DefaultGenerics` type
};

Lack of Ease of Use

Integrators would have to import and apply generics to multitude of places to get proper type support.

custom-attachment.tsx
import type { LocalStreamChatGenerics } from "./types.ts";

export const CustomAttachment = (
  props: AttachmentProps<LocalStreamChatGenerics>,
) => {
  const { client } = useChatContext<LocalStreamChatGenerics>();

  console.log(client.user?.profilePicture);

  const [attachment] = props.attachments;

  if (typeof attachment.myCustomThing === "string") {
    return <div>...</div>;
  }

  return <Attachment {...props} />;
};
root.tsx
import { CustomAttachment } from "./custom-attachment.tsx";
import type { LocalStreamChatGenerics } from "./types.ts";

const Root = () => {
  const chatClient = useCreateChatClient<LocalStreamChatGenerics>({
    apiKey: "<api-key>",
    tokenOrProvider: "user.access.token",
    userData: { id: "<user-id>" },
  });

  return (
    <Chat client={chatClient}>
      <Channel Attachment={CustomAttachment}>...</Channel>
    </Chat>
  );
};

Replacement - Module Augmentation

The transition to the new mechanism is fairly easy and pretty straightforward. Almost all of your custom declarations can stay as is, we’ll just have to move them to a declaration file. In there we’ll import stream-chat and define a specific interface to extend.

stream-custom-data.d.ts
import "stream-chat";

declare module "stream-chat" {
  // your "attachmentType" now becomes
  interface CustomAttachmentData {
    custom_property?: string;
  }

  // your "userType" now becomes
  interface CustomUserData {
    profile_picture?: string;
  }

  interface CustomCommandData {
    "custom-command": unknown;
    "other-custom-command": unknown;
  }
}

CustomCommandType

Note that commands (CustomCommandType) are a special case which transforms from string union type (commandType: 'custom-command' | 'other-custom-command') to an interface from which only the keys would be used, the value type is not important and unknown would suffice.

ChannelResponse.name

If you wish to continue using ChannelResponse.name property, you’ll have to add it yourself by extending CustomChannelData interface since it’s considered to be a custom field and is not part of the Channel entity by default.

Now as for the rest of your code; none of the members exported by the stream-chat-react (and stream-chat) no longer support StreamChatGenerics which means you’ll have to remove them. Using our previous example your code should now look like this:

custom-attachment.tsx
export const CustomAttachment = (props: AttachmentProps) => {

  const { client } = useChatContext(); 

  console.log(client.user?.profile_picture);

  const [attachment] = props.attachments;

  if (typeof attachment.custom_property === "string") {
    return <div>...</div>;
  }

  return <Attachment {...props} />;
};
root.tsx
import { CustomAttachment } from "./custom-attachment.tsx";

const Root = () => {
  const chatClient = useCreateChatClient({

    apiKey: "<api-key>",
    tokenOrProvider: "user.access.token",
    userData: {
      id: "<user-id>",
      profile_picture: "https://random.picture/1.jpg",
    },
  });

  return (
    <Chat client={chatClient}>
      <Channel Attachment={CustomAttachment}>...</Channel>
    </Chat>
  );
};

And that’s it! One place, one definition, no need to pass anything else around your code. Everything is taken care of internally and all of your custom types are now accounted for.

If you don’t care about specifying your custom types and prefer the previous behavior (casting) you can mark every unspecified key as unknown:

stream-custom-data.d.ts
import "stream-chat";

declare module "stream-chat" {
  interface CustomAttachmentData {
    [key: string]?: unknown;
  }
}

Now this is generally not recommended, but it is a possibility.

Removal of the jsDelivr (UMD) Bundle

The package provided a bundle which was built using an outdated UMD format. If your intention was to use stream-chat imported directly from the CDN, see esm.sh documentation for a viable alternative.

import { StreamChat } from "https://esm.sh/stream-chat";

Replacement of FormatMessageResponse by LocalMessage type

The new type LocalMessage is used everywhere a local state message object representation is required. In general, if a message response object obtained from the server is to be stored in the state, then it has to be converted into LocalMessage. The new type contains key error to store the information about failed message creation server-side. Type FormatMessageResponse is an alias to LocalMessage and will be removed in the future.

With this change, the signature of StreamChat.updateMessage() method has been changed. The first argument - message - should be of type LocalMessage | Partial<MessageResponse> instead of UpdatedMessage.

© Getstream.io, Inc. All Rights Reserved.