This is beta documentation for Stream Chat React Native SDK v9. For the latest stable version, see the latest version (v8) .

Custom Poll Flow

This cookbook shows how to customize a Channel screen with the Poll component and use navigation instead of modals.

Best Practices

  • Enable polls at the dashboard level before debugging UI behavior.
  • Keep poll screens inside Channel so they inherit the correct contexts.
  • Reuse default poll subcomponents and override only the pieces you need.
  • Route poll navigation explicitly to avoid mixing modal and stack flows.
  • Handle poll state updates through provided hooks instead of manual mutations.

Prerequisites

A Channel screen with a MessageList is required. Polls are tied to messages, so this must be in place first.

Polls are disabled by default. Enable them in the Stream Dashboard by toggling the Poll button.

Enable Polls in Dashboard

This adds the Poll button to your attachment picker:

Poll button in attachment picker

You also need a working chatClient. Examples use React Navigation, but any navigation library works.

The Initial UI

Without any customization, you should have something similar to the following:

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-react-native";

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel}>
          <ChannelHeader />
          <MessageList />
          <MessageInput />
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

The poll creation flow works out of the box with the default UI:

Default poll in message list

Default poll creation screen

Default poll results modal

Customizing Poll Content

Customize the poll UI inside messages by setting PollContent on Channel. Start by removing the poll header and adjusting the bottom buttons. Reuse the default PollContent and override only the pieces you need.

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
} from "stream-chat-react-native";

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
        }
      />
      <EndVoteButton />
    </>
  );
};

const MyPollContent = () => (
  <PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelHeader />
          <MessageList />
          <MessageInput />
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Poll with custom buttons and hidden header

Poll with View Results alert

Now only two buttons render (View Results and End Vote). View Results shows an Alert instead of opening the modal.

Next, reintroduce the PollResults screen using your own navigation instead of React Native Modals.

Since all Poll screens need to be children of the Channel component, introduce an additional Stack navigator inside Channel:

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
        }
      />
      <EndVoteButton />
    </>
  );
};

const MyPollContent = () => (
  <PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);

const ChannelMessageList = () => {
  return (
    <>
      <ChannelHeader />
      <MessageList />
      <MessageInput />
    </>
  );
};

const ChannelStack = createStackNavigator<StackNavigatorParamList>();

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={() => null}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

For now, leave PollResultsScreen empty. This stack inside Channel can host all poll screens while keeping channel and other Channel customizations in one place.

Building the Results Screen

Reconstruct the PollResults screen using the PollModalHeader and PollResults components to get the default UI out of the box:

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
  PollResults,
  PollModalHeader,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

// ... rest of the components

const PollResultsScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader title={'RESULTS'} onPress={() => navigation.goBack()} />
      <PollResults message={message} poll={poll} />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
            <ChannelStack.Screen
              name={'ChannelMessageList'}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={'PollResultsScreen'}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

View Results now navigates to PollResults in the stack instead of a modal:

Poll results screen with custom header

The UI matches the default, but the title is now RESULTS.

Pinning the Poll Name as Header

For long option lists, pin the poll name at the top using the usePollState hook and the PollResultsContent component:

import {
  // ...rest of the imports
  usePollState,
  PollResultsContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const MyPollResultsContent = () => {
  const { name } = usePollState();
  const navigation = useNavigation();
  return (
    <>
      <PollModalHeader title={name} onPress={() => navigation.goBack()} />
      <PollResultsContent />
    </>
  );
};

const PollResultsScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollResults
        message={message}
        poll={poll}
        PollResultsContent={MyPollResultsContent}
      />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Poll results with poll name pinned as header

This works because PollResults is wrapped in PollContext, and the button callback provides the data needed to initialize the provider.

Custom Poll Creation Screen

Apply the same navigation pattern to poll creation for consistency:

import {
  // ...rest of the imports
  CreatePoll,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const MyCreatePollContent = ({
  route: {
    params: { sendMessage },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <CreatePoll
        sendMessage={sendMessage}
        closePollCreationDialog={() => navigation.goBack()}
      />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel
          channel={channel}
          PollContent={MyPollContent}
          openPollCreationDialog={({ sendMessage }) =>
            navigation.navigate("CreatePollScreen", { sendMessage })
          }
        >
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
            <ChannelStack.Group screenOptions={{ presentation: "modal" }}>
              <ChannelStack.Screen
                name={"CreatePollScreen"}
                options={{ headerShown: false }}
                component={MyCreatePollContent}
              />
            </ChannelStack.Group>
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Custom poll creation screen via navigation

Theming Poll Components

All Poll components are theme-compatible. Change the background color by overriding the default theme:

import {
  // ...rest of the imports
  ThemeProvider,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const myTheme: DeepPartial<Theme> = {
  poll: {
    message: {
      container: {
        backgroundColor: "pink",
      },
    },
  },
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <ThemeProvider style={myTheme}>
      <OverlayProvider>
        <Chat client={client}>
          <Channel
            channel={channel}
            PollContent={MyPollContent}
            openPollCreationDialog={({ sendMessage }) =>
              navigation.navigate("CreatePollScreen", { sendMessage })
            }
          >
            <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
              <ChannelStack.Screen
                name={"ChannelMessageList"}
                options={{ headerShown: false }}
                component={ChannelMessageList}
              />
              <ChannelStack.Screen
                name={"PollResultsScreen"}
                options={{ headerShown: false }}
                component={PollResultsScreen}
              />
              <ChannelStack.Group screenOptions={{ presentation: "modal" }}>
                <ChannelStack.Screen
                  name={"CreatePollScreen"}
                  options={{ headerShown: false }}
                  component={MyCreatePollContent}
                />
              </ChannelStack.Group>
            </ChannelStack.Navigator>
          </Channel>
        </Chat>
      </OverlayProvider>
    </ThemeProvider>
  );
};

Poll with custom pink background

Set the theme above your OverlayProvider so that poll customizations are also reflected on the message preview when it is long pressed.

Custom Poll Answers List

If the default PollAnswersList UI doesn't fit your needs, you can replace it.

Adding Comment Buttons

First, add ShowAllCommentsButton and AddCommentButton to your custom buttons:

import {
  // ...rest of the imports
  ShowAllCommentsButton,
  AddCommentButton
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

// ... rest of the components

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ShowAllCommentsButton />
      <AddCommentButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

// ... the ChannelScreen component

To create comments, enable answers/comments when creating the poll, then use the Add Comment button:

Poll creation with comments enabled

Adding a comment to a poll

Add a dedicated navigation screen for AnswersList, starting with the default UI:

import {
  // ...rest of the imports
  PollAnswersList,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

// ... rest of the components

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ShowAllCommentsButton
          onPress={({ message, poll }) => {
            navigation.navigate('PollAnswersScreen', {
              message,
              poll,
            });
          }}
        />
      <AddCommentButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

const PollAnswersScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader title={'All Poll Answers'} onPress={() => navigation.goBack()} />
      <PollAnswersList message={message} poll={poll} />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <ThemeProvider style={myTheme}>
      <OverlayProvider>
        <Chat client={client}>
          <Channel
            channel={channel}
            PollContent={MyPollContent}
            openPollCreationDialog={({ sendMessage }) => navigation.navigate('CreatePollScreen', { sendMessage })}
          >
            <ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
              <ChannelStack.Screen
                name={'ChannelMessageList'}
                options={{ headerShown: false }}
                component={ChannelMessageList}
              />
              <ChannelStack.Screen
                name={'PollResultsScreen'}
                options={{ headerShown: false }}
                component={PollResultsScreen}
              />
              <ChannelStack.Screen
                name={'PollAnswersScreen'}
                options={{ headerShown: false }}
                component={PollAnswersScreen}
              />
              <ChannelStack.Group screenOptions={{ presentation: 'modal' }}>
                <ChannelStack.Screen
                  name={'CreatePollScreen'}
                  options={{ headerShown: false }}
                  component={MyCreatePollContent}
                />
              </ChannelStack.Group>
            </ChannelStack.Navigator>
          </Channel>
        </Chat>
      </OverlayProvider>
    </ThemeProvider>
  );
};

This allows navigation to the default PollAnswersList UI:

Default poll answers list screen

Overriding the Answers List Content

Override PollAnswersListContent to fully customize the UI. Since the list of answers can be large, use the usePollAnswersPagination hook for pagination:

import {
  // ...rest of the imports
  usePollAnswersPagination,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const LoadingIndicator = () => {
  /* some LoadingIndicator logic here */
};

const MyItem = ({ item }) => {
  const { answer_text, user } = item;
  return (
    <Text>
      {user.name} commented: {answer_text}
    </Text>
  );
};

const MyPollAnswersContent = () => {
  const { pollAnswers, loading, loadMore } = usePollAnswersPagination();
  return (
    <FlatList
      contentContainerStyle={{ flex: 1, padding: 16 }}
      data={pollAnswers}
      renderItem={MyItem}
      onEndReached={loadMore}
      ListFooterComponent={loading ? <LoadingIndicator /> : null}
    />
  );
};

const PollAnswersScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader
        title={"All Poll Answers"}
        onPress={() => navigation.goBack()}
      />
      <PollAnswersList
        message={message}
        poll={poll}
        PollAnswersListContent={MyPollAnswersContent}
      />
    </SafeAreaView>
  );
};

// ... the Channel screen

Custom poll answers list with pagination

The list supports pagination on scroll and displays a loading indicator when fetching more answers.

Reusable Components

Any components from the default UI can be imported from the SDK. These include:

  • All buttons mentioned here
  • CreatePollContent
  • PollContent
  • PollButtons
  • PollHeader
  • PollModalHeader
  • PollInputDialog
  • CreatePollIcon
  • PollOption
  • PollResultsContent
  • PollResultsItem
  • PollVote
  • PollAllOptionsContent