---
id: persist_input_text_in_localstorage
slug: /guides/persist-input-text-in-localstorage
title: 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
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));
};

// highlight-start
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,
  };
};
// highlight-end
```

## 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
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
          //   highlight-start
          additionalTextareaProps={{ onChange: handleInputChange }}
          getDefaultValue={getMainInputDraft}
          overrideSubmitHandler={overrideSubmitHandler}
          //   highlight-end
          focus
        />
      </Window>
      <Thread
        additionalMessageInputProps={{
          //   highlight-start
          additionalTextareaProps: { onChange: handleThreadInputChange },
          getDefaultValue: getThreadInputDraft,
          overrideSubmitHandler,
          //   highlight-end
        }}
      />
    </>
  );
};

// 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.
