# Polls

A poll is a question with options that the channel's members vote on. It's created on the server first, and then its id is attached to the sent message. While the user fills in the poll form, the poll composer, `messageComposer.pollComposer`, holds the form and checks each field.

>
> **Info:** **Example**
>
> The user has typed "One more thing" in the channel's message input but hasn't sent it. The text is in the channel composer's text composer, `messageComposer.textComposer`. Then they open the poll creation sheet from the attachment picker. The sheet resets the poll form when it closes, so the form starts empty:
>
> 1. In the form, they type the question "Lunch?" and the options "Pizza" and "Sushi". Each change goes into the poll composer through `PollComposer.updateFields()`. The text in the message input doesn't change. Once the question and an option are filled in, `pollComposer.canCreatePoll` becomes `true`.
> 2. They press Create in the sheet, and the sheet makes two calls. `MessageComposer.createPoll()` creates the poll on the server and stores its id in `messageComposer.pollId`. Then `MessageComposer.send()` sends a message that carries only the poll.
> 3. After the send, `messageComposer.pollId` is empty again. "One more thing" stays in the composer, ready to send as the next message.
>

## What it holds

The poll composer's state is in `pollComposer.state`. It has two fields: `data` (stores the form's state), and `errors`, the problems found in it.

`data` holds the poll's fields:

| Field                          | Starts as        | What it holds                                                                                                                                |
| ------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                         | `''`             | The question. A poll can't be created without one.                                                                                           |
| `description`                  | `''`             | More about the question.                                                                                                                     |
| `options`                      | one empty option | The options, each `{ id, text }`. The last one is kept empty, for the user to type the next option into.                                     |
| `enforce_unique_vote`          | `true`           | Each member votes for one option only.                                                                                                       |
| `max_votes_allowed`            | `''`             | How many options a member can vote for, as a string from `'2'` to `'10'`. `''` means no limit. Used only while `enforce_unique_vote` is off. |
| `allow_answers`                | `false`          | Members can comment on the poll, one comment each, which is called an answer. Answers don't count as votes.                                  |
| `allow_user_suggested_options` | `false`          | Members can add options.                                                                                                                     |
| `voting_visibility`            | `'public'`       | `'anonymous'` hides who voted for what.                                                                                                      |
| `id`                           | a new id         | The id the poll gets on the server.                                                                                                          |

Each field has a getter that reads its current value, such as `pollComposer.name` or `pollComposer.options`. `pollComposer.canCreatePoll` says whether the form is complete: the question is set, at least one option has text, `max_votes_allowed` is empty or a number from 2 to 10, and `pollComposer.state.errors` holds no [validation error](#validation-errors).

## Filling in the form

Each change passes the form through the **poll middleware**: a pipeline of functions that processes the changed fields and checks them, as [Composer middleware](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/composer-middleware/#built-in-chains) explains.

What a poll form needs, and the call for each (the last column says which middleware handler a call runs):

| To...                                 | Call                                                                 | What happens                                                                                                                                                                                | Poll middleware     |
| ------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| change fields as the user types       | `PollComposer.updateFields({ data })`                                | Updates the fields in `data` and checks them, as below.                                                                                                                                     | `handleFieldChange` |
| change the text of one option         | `PollComposer.updateFields({ data: { options: { index, text } } })`  | Changes the option at `index`. An option emptied before the last one is removed. A new empty option is added at the end when none is empty, up to 100 options.                              | `handleFieldChange` |
| reorder the options                   | `PollComposer.updateFields({ data: { options } })`                   | Replaces the options with the array, in its order.                                                                                                                                          | `handleFieldChange` |
| check a field once the user leaves it | `PollComposer.handleFieldBlur(field)`, from the field's blur handler | Adds the errors that only make sense once the user is done with the field, such as an empty question, to `pollComposer.state.errors`. A form of your own calls it when a field loses focus. | `handleFieldBlur`   |
| start a new poll                      | `PollComposer.initState()`                                           | Resets the form to its starting values. Sending a poll doesn't reset the form, so call it when a new poll form opens or closes.                                                             | none                |

The SDK's creation sheet calls `PollComposer.updateFields()` as the user types, `PollComposer.handleFieldBlur()` when the user leaves the question, an option or the maximum number of votes, and `PollComposer.initState()` when it closes.

```ts
const { pollComposer } = channel.messageComposer;

await pollComposer.updateFields({ data: { name: "Lunch?" } });
await pollComposer.updateFields({
  data: { options: { index: 0, text: "Pizza" } },
});
```

While processing a change, the built-in middleware keeps the form consistent: changing `enforce_unique_vote` empties `max_votes_allowed`, and `max_votes_allowed` is turned into a whole number from 2 to 10, or `''` when it isn't a number.

`PollComposer.updateFields()` also takes `injectedFieldErrors`, errors of your own, for example from a check performed by your own logic. They win over the errors the middleware finds.

For example, to reject questions that your own check doesn't allow:

```ts
import type { MessageComposer, PollComposerValidationCode } from "stream-chat";

// Your own check, for example a request to your server.
const isQuestionAllowed = async (name: string) => !name.includes("spam");

const changeQuestion = async (composer: MessageComposer, name: string) => {
  const allowed = await isQuestionAllowed(name);

  await composer.pollComposer.updateFields({
    data: { name },
    injectedFieldErrors: {
      name: allowed
        ? undefined
        : {
            // `code` takes only the built-in codes, so a code of your own needs a cast.
            code: "myapp:poll:name:notAllowed" as PollComposerValidationCode,
            message: "This question isn't allowed",
          },
    },
  });
};
```

The error is saved to `pollComposer.state.errors.name`, and `pollComposer.canCreatePoll` stays `false` while it's there. Passing `undefined` for a field clears your error.

## Validation errors

While the user fills in the form, the poll composer saves field errors in `pollComposer.state.errors`, for your UI to show next to the fields. While any error is there, `pollComposer.canCreatePoll` is `false`.

Subscribe to `pollComposer.state` to show the errors as they change:

```ts
const { pollComposer } = channel.messageComposer;

const unsubscribe = pollComposer.state.subscribeWithSelector(
  ({ errors }) => ({ errors }),
  ({ errors }) => {
    // The question's error, or undefined when the question is fine.
    if (errors.name) {
      console.log(errors.name.code, errors.name.message);
    }

    // One error per option that has a problem, keyed by the option's id.
    Object.entries(errors.options ?? {}).forEach(([optionId, error]) => {
      console.log(optionId, error.code, error.message);
    });
  },
);
```

For example, after the user leaves the question empty and types "Pizza" into two options, `pollComposer.state.errors` is:

```json
{
  "name": {
    "code": "validation:poll:name:required",
    "message": "Question is required"
  },
  "options": {
    "<id of the second Pizza option>": {
      "code": "validation:poll:option:duplicate",
      "message": "Option already exists"
    }
  }
}
```

`code` says what's wrong: translate it. `message` is in English, to show when you have no translation.

The table lists every `code`, and where to read it:

| `code`                                        | Read from                   | Found when                                         | Means                                                   |
| --------------------------------------------- | --------------------------- | -------------------------------------------------- | ------------------------------------------------------- |
| `validation:poll:name:required`               | `errors.name`               | the user leaves the question                       | The question is empty.                                  |
| `validation:poll:option:duplicate`            | `errors.options[option.id]` | an option changes, or the user leaves the options  | The option has the same text as an option before it.    |
| `validation:poll:option:empty`                | `errors.options[option.id]` | the user leaves the options                        | An option other than the last one is empty.             |
| `validation:poll:maxVotes:uniqueVoteEnforced` | `errors.max_votes_allowed`  | `max_votes_allowed` changes                        | A vote limit was set while `enforce_unique_vote` is on. |
| `validation:poll:maxVotes:notNumeric`         | `errors.max_votes_allowed`  | `max_votes_allowed` changes                        | The limit isn't a number.                               |
| `validation:poll:maxVotes:outOfRange`         | `errors.max_votes_allowed`  | `max_votes_allowed` changes, or the user leaves it | The limit isn't from 2 to 10.                           |

The built-in processing already turns `max_votes_allowed` into a number from 2 to 10, so the last two appear only with processors of your own.

The SDK's creation sheet shows an option's error under the option. For the built-in `optionDuplicate` and `optionEmpty` codes it shows its own translated text, and for any other code the error's `message`. It shows no text for an error of the question or of `max_votes_allowed`. Any error disables the send button, because `PollComposer.canCreatePoll` is `false` while an error is set.

## Validators and processors

`PollComposer.updateFields()` runs `handleFieldChange` on the poll state chain, with the fields it changes. `PollComposer.handleFieldBlur(field)` runs `handleFieldBlur` with one field.

For each field in the change, the built-in middleware runs the field's processor, then the field's validator:

- A processor receives `{ data, value }` and returns the fields to store, such as `{ name: value.trim() }`. It can return more than one field.
- A validator receives `{ data, value, currentError }` and returns errors by field. `{ name: undefined }` clears the question's error. It can set or clear the errors of other fields too.

`data` is the poll before the change, `value` is the field's new value after its processor, and `currentError` is the field's error before the change. The built-in functions are exported:

| Event               | Processors                                                                                                                                                                                                                                                       | Validators                                                                                                                                                                                               |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handleFieldChange` | `pollCompositionStateProcessors`. They keep `max_votes_allowed` between 2 and 10, empty it when `enforce_unique_vote` changes, remove an option the user empties unless it's the last, and add an empty option at the end when none is empty, up to 100 options. | `pollStateChangeValidators` and `defaultPollFieldChangeEventValidators`. They flag duplicate options and an invalid `max_votes_allowed`, and clear the question's error once the user types.             |
| `handleFieldBlur`   | None.                                                                                                                                                                                                                                                            | `pollStateChangeValidators` and `defaultPollFieldBlurEventValidators`. They flag duplicate options and an invalid `max_votes_allowed`, require a question, and flag an empty option other than the last. |

`createPollComposerStateMiddleware({ processors, validators })` takes your functions by event, then by field, as in `{ handleFieldBlur: { name: validateName } }`. Each one replaces the built-in function for the same field and event, and the other built-in functions keep running. To keep a built-in check, call the built-in function from yours. A processor of your own for `options` replaces the one that removes and adds options. Its `value` is the whole list when the options are reordered, and `{ index, text }` when one option changes. `isTargetedOptionTextUpdate(value)` tells them apart.

An error is a `PollComposerValidationError`, `{ code, message, metadata? }`. `stream-chat` builds its own with `pollComposerValidationError(code)`, and the type of `code` allows only the codes in `POLL_COMPOSER_VALIDATION_CODE`. For a check of your own, give the error a code of your own, cast to `PollComposerValidationCode`, and the text to show as `message`. A prefix such as `my-app:` keeps your codes apart from the built-in ones.

This setup limits each option to 40 characters, and keeps the built-in checks for duplicate and empty options:

```ts
import {
  createPollComposerStateMiddleware,
  defaultPollFieldBlurEventValidators,
  pollStateChangeValidators,
  StreamChat,
} from "stream-chat";
import type {
  PollComposerOption,
  PollComposerValidationCode,
  PollComposerValidationError,
  PollStateChangeValidator,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

const MAX_OPTION_LENGTH = 40;

// The type of `code` allows only the built-in codes, so a code of your own is cast.
const ownError = (
  code: string,
  message: string,
): PollComposerValidationError => ({
  code: code as PollComposerValidationCode,
  message,
});

const withLengthCheck =
  (builtIn: PollStateChangeValidator | undefined): PollStateChangeValidator =>
  (params) => {
    const errors = { ...builtIn?.(params).options };
    const options: PollComposerOption[] = params.value;

    options.forEach((option) => {
      if (!errors[option.id] && option.text.length > MAX_OPTION_LENGTH) {
        errors[option.id] = ownError(
          "my-app:poll:option:tooLong",
          `Use at most ${MAX_OPTION_LENGTH} characters`,
        );
      }
    });

    return { options: Object.keys(errors).length > 0 ? errors : undefined };
  };

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const { stateMiddlewareExecutor } = composer.pollComposer;

  stateMiddlewareExecutor.replace([
    createPollComposerStateMiddleware({
      validators: {
        handleFieldChange: {
          options: withLengthCheck(pollStateChangeValidators.options),
        },
        handleFieldBlur: {
          options: withLengthCheck(defaultPollFieldBlurEventValidators.options),
        },
      },
    }),
  ]);

  return () =>
    stateMiddlewareExecutor.replace([createPollComposerStateMiddleware()]);
});
```

`MiddlewareExecutor.replace()` puts the new middleware in the place of the built-in one, because both have the same id, and the teardown puts a default one back.

## Creating and sending the poll

Creating the poll takes two steps:

```ts
const composer = channel.messageComposer;

await composer.createPoll(); // creates the poll on the server and sets composer.pollId
await composer.send(); // sends the message that carries the poll
```

`MessageComposer.createPoll()` does nothing while `pollComposer.canCreatePoll` is `false`. Otherwise it sends the form, without its empty options, to the server and stores the new poll's id in `messageComposer.pollId`. When the request fails, it publishes an `api:poll:create:failed` notification and throws.

While `messageComposer.pollId` is set, the composer has something to send. In a channel composer, `MessageComposer.send()` then sends a message that carries only the poll: no text, attachments, mentions, quoted message or custom data. The text, attachments and quoted message stay in the composer, ready to send as the next message, as [Messages with a poll](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/composer-middleware/#messages-with-a-poll) explains. After the send, `messageComposer.pollId` is cleared.

In a thread composer, and while a message is edited, the poll is sent with the rest of the message, and the composer clears after the send.

To send the poll together with the text and attachments in a channel too, remove the poll-only middleware. Its id is exported from `stream-chat` as `POLL_ONLY_COMPOSITION_MIDDLEWARE_ID`:

```ts
import { POLL_ONLY_COMPOSITION_MIDDLEWARE_ID, StreamChat } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  // A poll then goes out with the text and attachments, and the composer clears after the send.
  composer.compositionMiddlewareExecutor.remove(
    POLL_ONLY_COMPOSITION_MIDDLEWARE_ID,
  );
});
```

A [draft](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/message-composer/#drafts) keeps `messageComposer.pollId`, so a poll that was created but not sent comes back with the draft. The form in `pollComposer.state` isn't saved in it.

## When polls are on

Polls are on while `messageComposer.config.polls.enabled` is `true`. That's when both the `polls.enabled` setting in `client.config` and the channel type's `polls` setting are on. The composer doesn't check it when it creates or sends a poll: the UI reads it to decide whether to offer polls. `Channel` reads it to set `hasCreatePoll`, and the SDK's attachment picker offers polls only outside threads, when `hasCreatePoll` is `true` and the user has the `send-poll` capability.

## Settings

Set them for every composer in `client.config`, under `messageComposer.polls`:

```ts
client.config.set({
  messageComposer: {
    polls: { enabled: false },
  },
});
```

The poll composer has no setters. To change a setting for one composer only, patch that composer's configuration, for example `channel.messageComposer.updateConfig({ polls: { enabled: false } })`.

| Setting   | Default | What it does                                                               |
| --------- | ------- | -------------------------------------------------------------------------- |
| `enabled` | `true`  | `false` turns off polls. Combined with the channel type's `polls` setting. |

See [Polls](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/polls/) for the feature, including voting, and the [custom poll flow UI cookbook](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/screens-and-sheets/custom-poll-flow/) for a creation screen of your own.

## Used in

- [Polls](https://getstream.io/chat/docs/sdk/react-native/features/rich-content/polls/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/polls/](https://getstream.io/chat/docs/sdk/react-native/state/writing-messages/polls/).