# Storing message drafts

In this recipe, we would like to demonstrate how you can start storing unsent user's messages as drafts. The whole implementation turns around the use of `MessageInput`'s prop `getDefaultValue` and custom change event handler. We will store the messages in localStorage.

## Building the draft storage logic

Below, we have a simple logic to store all the message text drafts in a localStorage object under the key `@chat/drafts`.

```ts
const STORAGE_KEY = "@chat/drafts";

const getDrafts = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");

const removeDraft = (key: string) => {
  const drafts = getDrafts();

  if (drafts[key]) {
    delete drafts[key];
    localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
  }
};

const updateDraft = (key: string, value: string) => {
  const drafts = getDrafts();

  if (!value) {
    delete drafts[key];
  } else {
    drafts[key] = value;
  }

  localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
};
```

On top of this logic we build a hook that exposes the change handler functions for both thread and main `MessageInput` components as well as functions for `MessageInput`'s `getDefaultValue` prop. We also have to override the `MessageInput`'s default submit handler, because we want to remove the draft from storage when a message is sent.

```ts {30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76}
import { ChangeEvent, useCallback } from "react";
import {
  MessageToSend,
  useChannelActionContext,
  useChannelStateContext,
} from "stream-chat-react";
import type { Message } from "stream-chat";

const STORAGE_KEY = "@chat/drafts";

const getDrafts = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");

const removeDraft = (key: string) => {
  const drafts = getDrafts();

  if (drafts[key]) {
    delete drafts[key];
    localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
  }
};

const updateDraft = (key: string, value: string) => {
  const drafts = getDrafts();

  if (!value) {
    delete drafts[key];
  } else {
    drafts[key] = value;
  }

  localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts));
};

const useDraftAPI = () => {
  const { channel, thread } = useChannelStateContext();
  const { sendMessage } = useChannelActionContext();

  const handleInputChange = useCallback(
    (e: ChangeEvent<HTMLTextAreaElement>) => {
      updateDraft(channel.cid, e.target.value);
    },
    [channel.cid],
  );

  const handleThreadInputChange = useCallback(
    (e: ChangeEvent<HTMLTextAreaElement>) => {
      if (!thread) return;
      updateDraft(`${channel.cid}:${thread.id}`, e.target.value);
    },
    [channel.cid, thread],
  );

  const getMainInputDraft = useCallback(() => {
    const drafts = getDrafts();
    return drafts[channel.cid] || "";
  }, [channel.cid]);

  const getThreadInputDraft = useCallback(() => {
    if (!thread) return;
    const drafts = getDrafts();
    return drafts[`${channel.cid}:${thread.id}`] || "";
  }, [channel.cid, thread]);

  const overrideSubmitHandler = useCallback(
    async (
      message: MessageToSend,
      channelCid: string,
      customMessageData?: Partial<Message>,
    ) => {
      await sendMessage(message, customMessageData);
      const key = message.parent
        ? `${channelCid}:${message.parent.id}`
        : channelCid;
      removeDraft(key);
    },
    [sendMessage],
  );

  return {
    getMainInputDraft,
    getThreadInputDraft,
    handleInputChange,
    handleThreadInputChange,
    overrideSubmitHandler,
  };
};
```

## Plugging it in

Now it is time to access the API in our React component. The component has to be a descendant of `Channel` component, because `useDraftAPI` accesses the `ChannelStateContext` and `ChannelActionContext` through corresponding consumers. In our example we call this component `ChannelWindow`.

```tsx {21,22,23,29,30,31}
import {
  ChannelFilters,
  ChannelOptions,
  ChannelSort,
  StreamChat,
} from "stream-chat";
import { useDraftAPI } from "./useDraftAPI";
import type { StreamChatGenerics } from "./types";

const ChannelWindow = () => {
  const {
    getMainInputDraft,
    getThreadInputDraft,
    handleInputChange,
    handleThreadInputChange,
    overrideSubmitHandler,
  } = useDraftAPI();

  return (
    <>
      <Window>
        <TruncateButton />
        <ChannelHeader />
        <MessageList />
        <MessageInput
          additionalTextareaProps={{ onChange: handleInputChange }}
          getDefaultValue={getMainInputDraft}
          overrideSubmitHandler={overrideSubmitHandler}
          focus
        />
      </Window>
      <Thread
        additionalMessageInputProps={{
          additionalTextareaProps: { onChange: handleThreadInputChange },
          getDefaultValue: getThreadInputDraft,
          overrideSubmitHandler,
        }}
      />
    </>
  );
};

// In your application you will probably initiate the client in a React effect.
const chatClient = StreamChat.getInstance<StreamChatGenerics>("<YOUR_API_KEY>");

// User your own filters, options, sort if needed
const filters: ChannelFilters = {
  type: "messaging",
  members: { $in: ["<YOUR_USER_ID>"] },
};
const options: ChannelOptions = { state: true, presence: true, limit: 10 };
const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };

const App = () => {
  return (
    <Chat client={chatClient}>
      <ChannelList
        filters={filters}
        sort={sort}
        options={options}
        showChannelSearch
      />
      <Channel>
        <ChannelWindow />
      </Channel>
    </Chat>
  );
};
```

Now once you start typing, you should be able to see the drafts in the `localStorage` under the key `@chat/drafts`. Despite changing channels or threads, the unsent message text should be kept in the textarea.


---

This page was last updated at 2026-07-10T16:05:10.098Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v11/guides/persist-input-text-in-localstorage/](https://getstream.io/chat/docs/sdk/react/v11/guides/persist-input-text-in-localstorage/).