Message Reminders

Message reminders let users revisit important messages later. With a timestamp, the user receives a notification at the scheduled time. Without one, the reminder acts like a bookmark.

Best Practices

  • Enable Push V3 and the reminder event before testing reminder delivery.
  • Keep reminder offsets limited to reduce UI clutter.
  • Use useMessageReminder for per-message updates instead of manual polling.
  • Sort reminders by remind_at for predictable list ordering.
  • Clear reminder indicators when a reminder is deleted or expired.

Push Notifications Setup

Reminders require Push V3. In the Stream Dashboard, go to Push Notifications and click Upgrade to V3.

Push V3

Then, in each configuration, enable notification.reminder_due in Configure Push Notification Templates.

Message Reminder UI Interaction

Users create, update, or delete reminders from the message actions menu.

The reminder indicator appears in the message UI (or disappears when deleted).

Get Message Reminder Data

The SDK uses client.reminders for CRUD. It stores a reactive map of message IDs to Reminder instances. Each Reminder extends ReminderResponse with timeLeftMs.

export type ReminderState = {
  channel_cid: string;
  created_at: Date;
  message: MessageResponse | null;
  message_id: string;
  remind_at: Date | null;
  timeLeftMs: number | null;
  updated_at: Date;
  user: UserResponse | null;
  user_id: string;
};
  1. Get a reminder for a specific message

Use useMessageReminder to get the Reminder instance for a message ID.

You can subscribe at two levels:

  1. Subscribe to a specific reminder’s state
import { useMessageReminder, useStateStore } from "stream-chat-react-native";
import type { LocalMessage, ReminderState } from "stream-chat";

const reminderStateSelector = (state: ReminderState) => ({
  timeLeftMs: state.timeLeftMs,
});

const Component = ({ message }: { message: LocalMessage }) => {
  // access the message reminder instance
  const reminder = useMessageReminder(message.id);
  const { timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};
};

timeLeftMs updates more frequently as the deadline approaches (daily → hourly → minutely) and less frequently afterward.

  1. Subscribe to reminders pagination

Pagination is handled by RemindersPaginator. Use useQueryReminders to fetch pages and react to updates and deletions.

Pagination returns ReminderResponse objects, not Reminder instances.

import { useCallback, useEffect } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import {
  useChatContext,
  useMessageReminder,
  useQueryReminders,
} from "stream-chat-react-native";
import { ReminderItem } from "./ReminderItem";

const renderItem = ({ item }: { item: ReminderResponse }) => (
  <ReminderItem {...item} />
);

const renderEmptyComponent = (
  <Text style={styles.emptyContainer}>No reminders available</Text>
);

const RemindersList = () => {
  const { client } = useChatContext();
  const { data, isLoading, loadNext } = useQueryReminders();

  useEffect(() => {
    client.reminders.paginator.filters = {};
    client.reminders.paginator.sort = { remind_at: 1 };
  }, [client.reminders]);

  const onRefresh = useCallback(async () => {
    await client.reminders.queryNextReminders();
  }, [client.reminders]);

  const renderFooter = useCallback(() => {
    if (isLoading) {
      return (
        <ActivityIndicator size={"small"} style={{ marginVertical: 16 }} />
      );
    }
  }, [isLoading]);

  return (
    <View style={{ flex: 1 }}>
      <FlatList
        contentContainerStyle={{ flexGrow: 1 }}
        data={data}
        keyExtractor={(item) => item.message.id}
        renderItem={renderItem}
        ListEmptyComponent={renderEmptyComponent}
        ListFooterComponent={renderFooter}
        onEndReached={loadNext}
      />
    </View>
  );
};

Message Reminder Configuration

Configure which reminder offsets users can select:

const minute = 60 * 1000;
client.reminders.updateConfig({
  scheduledOffsetsMs: [30 * minute, 60 * minute],
});

You can also set when the reminder stops refreshing “time since due”:

const day = 24 * 60 * 60 * 1000;
client.reminders.updateConfig({
  stopTimerRefreshBoundaryMs: day,
});

Reminder indicator on message

You can add a reminder indicator in the header by passing a custom MessageHeader to Channel:

Message Reminder Header

import {
  MessageFooterProps,
  Time,
  useMessageReminder,
  useStateStore,
  useTranslationContext,
} from "stream-chat-react-native";
import { ReminderState } from "stream-chat";
import { StyleSheet, Text, View } from "react-native";

const reminderStateSelector = (state: ReminderState) => ({
  timeLeftMs: state.timeLeftMs,
});

export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
  const messageId = message?.id ?? "";
  const reminder = useMessageReminder(messageId);
  const { timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};
  const { t } = useTranslationContext();

  const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
  const stopRefreshTimeStamp =
    reminder?.remindAt && stopRefreshBoundaryMs
      ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
      : undefined;

  const isBehindRefreshBoundary =
    !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;

  if (!reminder) {
    return null;
  }

  // This is for "Saved for Later"
  if (!reminder.remindAt) {
    return (
      <View>
        <Text style={styles.headerTitle}>🔖 Saved for Later</Text>
      </View>
    );
  }

  if (reminder.remindAt && timeLeftMs !== null) {
    return (
      <View style={styles.headerContainer}>
        <Time height={16} width={16} />
        <Text style={styles.headerTitle}>
          {isBehindRefreshBoundary
            ? t("Due since {{ dueSince }}", {
                dueSince: t("timestamp/ReminderNotification", {
                  timestamp: reminder.remindAt,
                }),
              })
            : t("Due {{ timeLeft }}", {
                timeLeft: t("duration/Message reminder", {
                  milliseconds: timeLeftMs,
                }),
              })}
        </Text>
      </View>
    );
  }
};

const styles = StyleSheet.create({
  headerContainer: {
    flexDirection: "row",
    alignItems: "center",
  },
  headerTitle: {
    fontSize: 14,
    fontWeight: "500",
    marginLeft: 4,
  },
});