Repository: Rapptz/discord.py

Issue:
Multiple selects error
### Summary

master commit breaks multiple selects

### Reproduction Steps

this [commit](https://github.com/Rapptz/discord.py/tree/d826f4f3a8d4d97d24499f5de5c51e11e640e98f) provides global state for `Select` object 

### Minimal Reproducible Code

_No response_

### Expected Results

No global state

### Actual Results

global state

### Intents

...

### System Information

...

### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

_No response_


Discussion:
I have this problem too :/
Could you give more context to the issue? The actual behavior you're experiencing? Code to reproduce it?

The commit goal was to *remove* global state, which I believe it did successfully, solving race conditions as described in the PR.
I'm fairly sure I tested this before submitting the PR, just tested again and I'm not experiencing any issues.

Test code:
```py
# generate 4 options labeled 1-4
options = [SelectOption(label=f'option {n}') for n in range(1, 5)]

class Select(ui.Select):
    async def callback(self, interaction: Interaction):
        original = self.values[0]
        await interaction.response.defer()
        await asyncio.sleep(10)
        after_sleep = self.values[0]
        await interaction.followup.send(f'{original = }\n{after_sleep = }')

class View(ui.View):
    def __init__(self):
        super().__init__()
        self.add_item(Select(options=options[:2]))  # first select has options 1 and 2
        self.add_item(Select(options=options[2:]))  # second select has options 3 and 4
```
In quick succession, all before the first followup was sent, I selected in order:
"option 1" from the first select
"option 4" from the second select
"option 2" from the first select
And these were the resulting followups:
![image](https://user-images.githubusercontent.com/8661717/183746858-b2d8f102-de76-4224-b991-e3f1d94b8168.png)

EDIT: I also repeated this test with both selects using `@ui.select()` inside the view and with one a subclass, one decorated. Both gave the same result as the initial test where both were using the subclass.
```
import sys

import discord
from discord import Intents
from discord.ext import commands

bot = commands.Bot(command_prefix='/', intents=Intents.all())


@bot.tree.command()
async def create_event(interaction: discord.Interaction):
    '''
    Creates embed message for event on server
    '''
    event_category = list(
        filter(lambda x: 'event' in x.name.lower(), interaction.guild.categories)
    )[0]

    class TestModal(discord.ui.Modal, title='Test Modal'):
        text_channel = discord.ui.Select(
            options=[
                discord.SelectOption(label=channel.name, value=channel.id)
                for channel in event_category.text_channels
            ],
            placeholder='Channel for event message',
        )
        voice_channel = discord.ui.Select(
            options=[
                discord.SelectOption(label=channel.name, value=channel.id)
                for channel in event_category.voice_channels
            ],
            min_values=0,
            placeholder='Voice channel for auto connect button',
        )

        async def on_submit(self, interaction: discord.Interaction):
            await interaction.guild.get_channel(int(self.text_channel.values[0])).send(
                content='test',
            )
            await interaction.response.send_message('message added', ephemeral=True)

    await interaction.response.send_modal(TestModal())


def run():
    bot.run('TOKEN')


if __name__ == '__main__':
    run()
    sys.exit(0)
```

Error:
```
Traceback (most recent call last):
  File "/home/randomneo/.virtualenvs/discord-edge/lib/python3.10/site-packages/discord/ui/modal.py", line 184, in _scheduled_task
    await self.on_submit(interaction)
  File "/home/randomneo/projects/personal/fucking-discord/main.py", line 37, in on_submit
    await interaction.guild.get_channel(int(self.text_channel.values[0])).send(
IndexError: list index out of range
```

If redefine function `Modal._refresh` to prevent calling of second `item._refresh_state` then no error
```
    def _refresh(self, components: Sequence[ModalSubmitComponentInteractionDataPayload]) -> None:
        for component in components:
            if component['type'] == 1:
                self._refresh(component['components'])
                return
            else:
                item = find(lambda i: i.custom_id == component['custom_id'], self._children)  # type: ignore
                if item is None:
                    _log.debug("Modal interaction referencing unknown item custom_id %s. Discarding", component['custom_id'])
                    continue
                item._refresh_state(component)  # type: ignore
```
So this is specifically regarding modals. All selects get the last one's values, instead of their own.
The error you're getting is because the last select is voice channels, which you selected no options for.
I guess this is confirmed, then.
@Puncher1 on which version/commit are you running? Issue reproducible after [this commit](https://github.com/Rapptz/discord.py/tree/d826f4f3a8d4d97d24499f5de5c51e11e640e98f)
I am also experiencing this bug in a modal select
All selects get the value of the last select
@Randomneo 
~~I'm running it on the latest commit, when I list my packages with `pip list` I get this~~

Can confirm this bug. It tries to send the message to the voice channel. It just doesn't throw any errors. Sorry for the confusion 

Resolving patch:
diff --git a/discord/ui/modal.py b/discord/ui/modal.py
--- a/discord/ui/modal.py
+++ b/discord/ui/modal.py
@@ -174,9 +174,11 @@ def _refresh(self, components: Sequence[ModalSubmitComponentInteractionDataPaylo
                     continue
                 item._refresh_state(component)  # type: ignore
 
-    async def _scheduled_task(self, interaction: Interaction):
+    async def _scheduled_task(self, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]):
         try:
             self._refresh_timeout()
+            self._refresh(components)
+
             allow = await self.interaction_check(interaction)
             if not allow:
                 return
@@ -189,8 +191,10 @@ async def _scheduled_task(self, interaction: Interaction):
             # In the future, maybe this will require checking if we set an error response.
             self.stop()
 
-    def _dispatch_submit(self, interaction: Interaction) -> None:
-        asyncio.create_task(self._scheduled_task(interaction), name=f'discord-ui-modal-dispatch-{self.id}')
+    def _dispatch_submit(
+        self, interaction: Interaction, components: List[ModalSubmitComponentInteractionDataPayload]
+    ) -> None:
+        asyncio.create_task(self._scheduled_task(interaction, components), name=f'discord-ui-modal-dispatch-{self.id}')
 
     def to_dict(self) -> Dict[str, Any]:
         payload = {
diff --git a/discord/ui/select.py b/discord/ui/select.py
--- a/discord/ui/select.py
+++ b/discord/ui/select.py
@@ -23,7 +23,7 @@
 """
 
 from __future__ import annotations
-from typing import List, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Callable, Union
+from typing import List, Literal, Optional, TYPE_CHECKING, Tuple, TypeVar, Callable, Union, Dict
 from contextvars import ContextVar
 import inspect
 import os
@@ -54,7 +54,7 @@
 
 V = TypeVar('V', bound='View', covariant=True)
 
-selected_values: ContextVar[Optional[List[str]]] = ContextVar('selected_values', default=None)
+selected_values: ContextVar[Dict[str, List[str]]] = ContextVar('selected_values')
 
 
 class Select(Item[V]):
@@ -126,6 +126,7 @@ def __init__(
             disabled=disabled,
         )
         self.row = row
+        self._values: List[str] = []
 
     @property
     def custom_id(self) -> str:
@@ -262,8 +263,8 @@ def disabled(self, value: bool) -> None:
     @property
     def values(self) -> List[str]:
         """List[:class:`str`]: A list of values that have been selected by the user."""
-        values = selected_values.get()
-        return values if values is not None else []
+        values = selected_values.get(None)
+        return self._values if values is None else values.get(self.custom_id, [])
 
     @property
     def width(self) -> int:
@@ -276,7 +277,9 @@ def _refresh_component(self, component: SelectMenu) -> None:
         self._underlying = component
 
     def _refresh_state(self, data: MessageComponentInteractionData) -> None:
-        selected_values.set(data.get('values', []))
+        values = selected_values.get({})
+        self._values = values[self.custom_id] = data.get('values', [])
+        selected_values.set(values)
 
     @classmethod
     def from_component(cls, component: SelectMenu) -> Self:
diff --git a/discord/ui/view.py b/discord/ui/view.py
--- a/discord/ui/view.py
+++ b/discord/ui/view.py
@@ -416,6 +416,8 @@ async def _scheduled_task(self, item: Item, interaction: Interaction):
             if self.timeout:
                 self.__timeout_expiry = time.monotonic() + self.timeout
 
+            item._refresh_state(interaction.data)  # type: ignore
+
             allow = await self.interaction_check(interaction)
             if not allow:
                 return
@@ -615,7 +617,6 @@ def dispatch_view(self, component_type: int, custom_id: str, interaction: Intera
         if item is None:
             return
 
-        item._refresh_state(interaction.data)  # type: ignore
         # Note, at this point the View is *not* None
         item.view._dispatch_item(item, interaction)  # type: ignore
 
@@ -630,8 +631,7 @@ def dispatch_modal(
             _log.debug("Modal interaction referencing unknown custom_id %s. Discarding", custom_id)
             return
 
-        modal._refresh(components)
-        modal._dispatch_submit(interaction)
+        modal._dispatch_submit(interaction, components)
 
     def remove_interaction_mapping(self, interaction_id: int) -> None:
         # This is called before re-adding the view


Test patch: