Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Drafts

What users get

A message the user started but didn't send is kept as a draft on the server. The user types half a message in "Design" and leaves the channel. The next time the app loads "Design", on this device or another one, the text is back in the composer.

  • Each channel has its own draft, and so does each thread.
  • A draft keeps the text and mentions, uploaded attachments, link previews, the quoted message and a poll.
  • Sending the message, or clearing the composer, deletes the draft.
  • A draft saved on another device or tab appears in the composer while the channel is open.

How it works across the layers

Drafts belong to the composer, in the state layer. The UI only decides when to save one.

Saving

  1. The user leaves "Design". MessageComposer unmounts and calls MessageComposer.createDraft(), then clears the composer.
  2. MessageComposer.createDraft() builds the draft through the composer's draft middleware and saves it with Channel.createDraft(): for the channel, or for a thread when the composer belongs to one.
  3. Nothing is saved when the composer is empty, or when nothing changed since the draft was loaded.

Edits are never saved as drafts, and neither are messages in a channel that the server hasn't created yet.

Restoring

  • Channels. The server returns a channel's draft with the channel, whenever the channel is queried or watched: when a channel list loads or reloads, and after a reconnect. The composer loads the draft into its state.
  • Threads. A thread from the thread list carries its draft. When MessageComposer mounts for a thread whose composer is empty, it fetches the thread's draft with Channel.getDraft({ parent_id }).
  • Other devices. While drafts are on, the composer listens for draft.updated and draft.deleted. It loads an updated draft, and clears itself when its draft is deleted elsewhere. It ignores both while the user is editing a message.

Deleting

When the composer becomes empty, MessageComposer.deleteDraft() deletes the saved draft. Sending empties the composer, so a sent message takes its draft with it.

Draft operations change local state first and send the request after; see What's optimistic and what isn't. The composer's side is on Drafts.

Limitations

  • Opening a channel that is already loaded doesn't query it again. A user who leaves a channel and comes back in the same session finds the composer empty, because it was cleared when it unmounted, and the draft returns the next time the channel is queried. To load it on the spot, call MessageComposer.getDraft() when the composer mounts.
  • MessageComposer saves a draft when it unmounts. Closing the browser tab doesn't unmount it, so the text typed last is lost unless you save it yourself, as shown under Customizing.
  • Custom data sent with the draft isn't restored into the composer.
  • A thread draft saves the "Also send in channel" choice, but restoring the draft leaves the checkbox off.
  • The channel list doesn't show drafts.

Turning it on or off

Drafts are off by default. Turn them on with messageComposer.drafts.enabled:

client.config.set({ messageComposer: { drafts: { enabled: true } } });

Or pass the same settings to useCreateChatClient as config. The setting is client only: there is no server switch. It reaches every composer, in channels and threads alike.

Customizing

Saving when the page is hidden

Save the draft when the user switches tabs or closes the browser. Render this component next to the MessageComposer it should save, inside Channel or Thread:

import { useEffect } from "react";
import { useMessageComposerController } from "stream-chat-react";

export const SaveDraftWhenHidden = () => {
  const composer = useMessageComposerController();

  useEffect(() => {
    const saveDraft = () => {
      if (document.visibilityState === "hidden") void composer.createDraft();
    };
    document.addEventListener("visibilitychange", saveDraft);
    return () => document.removeEventListener("visibilitychange", saveDraft);
  }, [composer]);

  return null;
};

MessageComposer.createDraft() does nothing while drafts are off, and when there is nothing new to save.

Listing a user's drafts

client.queryDrafts() returns the current user's drafts across channels, for a "Drafts" screen. Each draft has its channel's channel_cid, a parent_id for a thread draft, and the draft message:

const { drafts } = await client.queryDrafts({
  limit: 20,
  sort: [{ direction: -1, field: "created_at" }],
});

for (const draft of drafts) {
  console.log(draft.channel_cid, draft.parent_id, draft.message.text);
}

It's a one-off query: nothing keeps the result current. See Paginators vs one-off queries.

Changing what a draft holds

Drafts are built by their own middleware chain. To add or drop data, add middleware to it; see Composer middleware.

Reference