Repository: plotly/dash

Issue:
Dash components with identical `id` are permitted
I am going to post the example which made me realize this as it also provide a reason to change the current situation. 

I wanted to update the value of a `dcc.Slider` connected to an instrument using a `dcc.Interval`. I noticed that because of the `dcc.Interval` my `dcc.DropDown`'s `value` was getting reset at each interval. So I decided to add a callback with the interval as an input to prevent the reset of the `dcc.DropDown`'s `value`. Unfortunately I had a `html.Button` with the same `id` (that was unintented) in my layout. This resulted in the freezing of the `dcc.DropDown`'s `value` to its initial `value`. I could still see the dropdown options and click on one, but the displayed value is Always `'COM1'`.

Here is a minimal example

````
# In[]:
# Import required libraries
import numpy as np
import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State, Event

app = dash.Dash('')
server = app.server
external_css = ["https://codepen.io/bachibouzouk/pen/ZRjdZN.css"]
for css in external_css:
    app.css.append_css({"external_url": css})

app.config.suppress_callback_exceptions = False
app.scripts.config.serve_locally = True

root_layout = html.Div(
    id='body',
    children=[
        dcc.Interval(id='interval', interval=5000),
        html.Div(
            id='page-content',
            children=[
                dcc.Dropdown(
                    id='instr_port',
                    options=[
                        {'label': "COM%i" % (i+1), 'value': "COM%i" % (i+1)}
                        for i in range(10)
                    ],
                    value='COM1'
                ),
                html.Button(
                    'Connect',
                    id='instr_port'

                ),
                # A series of gauges
                html.Div(
                    [
                        dcc.Slider(
                            id='gauge',
                            min=0,
                            max=10,
                            value=5,
                        )
                    ]
                )
            ]
        )
    ],
    style={'background': '#F3F6FA'}
)

# In[]:
# Create app layout
app.layout = root_layout


# In[]:
# Create callbacks
# generate the callbacks between the instrument and the app
@app.callback(
    Output('gauge', 'value'),
    [
        Input('interval', 'n_intervals')
    ]
)
def update_gauge(n_interval):
    """changes the value of the gauge periodically"""
    return 10 * np.random.random()


@app.callback(
    Output('instr_port', 'value'),
    [
        Input('interval', 'n_intervals')
    ],
    [
        State('instr_port', 'value')
    ]
)
def prevent_reset(n_interval, value):
    return value


if __name__ == '__main__':
    app.run_server(debug=False)
````

If I change the `id` of the `html.Button` then it works as I wanted it to work. 

It would be nice if Dash issued some warning if two or more components have the same `id`



Discussion:

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -843,13 +843,8 @@ def dispatch(self):
 
         return self.callback_map[target_id]['callback'](*args)
 
-    def _setup_server(self):
-        if self.config.include_assets_files:
-            self._walk_assets_directory()
-
-        # Make sure `layout` is set before running the server
-        value = getattr(self, 'layout')
-        if value is None:
+    def _validate_layout(self):
+        if self.layout is None:
             raise exceptions.NoLayoutException(
                 ''
                 'The layout was `None` '
@@ -857,6 +852,23 @@ def _setup_server(self):
                 'Make sure to set the `layout` attribute of your application '
                 'before running the server.')
 
+        layout_id = getattr(self.layout, 'id', None)
+
+        component_ids = {layout_id} if layout_id else set()
+        for component in self.layout.traverse():
+            component_id = getattr(component, 'id', None)
+            if component_id and component_id in component_ids:
+                raise exceptions.DuplicateIdError(
+                    'Duplicate component id found'
+                    ' in the initial layout: `{}`'.format(component_id))
+            component_ids.add(component_id)
+
+    def _setup_server(self):
+        if self.config.include_assets_files:
+            self._walk_assets_directory()
+
+        self._validate_layout()
+
         self._generate_scripts_html()
         self._generate_css_dist_html()
 
diff --git a/dash/exceptions.py b/dash/exceptions.py
--- a/dash/exceptions.py
+++ b/dash/exceptions.py
@@ -50,5 +50,9 @@ class PreventUpdate(CallbackException):
     pass
 
 
+class DuplicateIdError(DashException):
+    pass
+
+
 class InvalidCallbackReturnValue(CallbackException):
     pass
diff --git a/dash/version.py b/dash/version.py
--- a/dash/version.py
+++ b/dash/version.py
@@ -1 +1 @@
-__version__ = '0.24.1'
+__version__ = '0.24.2'


Test patch: