Repository: bokeh/bokeh

Issue:
[BUG] Can't add tabs after closing a tab
OSX 10.13, Python 3.6.10, Bokeh 2.1.1
(using Bokeh server)

Clicking on the button in the MWE below adds new tabs to the layout, as expected. However, whenever I close one of the new tabs, the button no longer works (the callback function isn't called).

```python
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models.widgets import Button
from bokeh.models import ColumnDataSource, Tabs, Panel, Range1d
from numpy.random import default_rng
rng = default_rng()

doc = curdoc()

generator_btn = Button(height=40, width=150, label="Add a new tab!")
generator_pnl = Panel(title="Start Here!", child=generator_btn)
tabs = Tabs(tabs=[generator_pnl], height=800, width=800)

class NewTab:
    def __init__(self):
        self.tab_num = 0
    
    def __call__(self):
        print("Making a new tab!")
        self.tab_num += 1
        xd = rng.random(10)
        yd = rng.random(10)
        source = ColumnDataSource(data={'x': xd, 'y': yd})
        axr = Range1d(start=0, end=1)
        fig = figure(title="Test of ad hoc tabs", x_range=axr, y_range=axr, 
                     plot_height=800, plot_width=800)
        fig.diamond(x='x', y='y', source=source)
        panel = Panel(title="New Tab #{}".format(self.tab_num), child=fig, 
                      closable=True)
        tabs.tabs.append(panel)

generator_btn.on_click(NewTab())

doc.add_root(tabs)
```



Discussion:
Settings tabs from bokehjs (by clicking close button) makes bokeh reinitialize all references associated with a patch (this includes the button), which clears `CallbackManager._event_callbacks` dict (`HasProps` mixin). I'm pretty sure this is a duplicate.
Also, `Button.clicks` is not properly defined (only declared), giving `NaN` value.

Resolving patch:
diff --git a/bokeh/core/has_props.py b/bokeh/core/has_props.py
--- a/bokeh/core/has_props.py
+++ b/bokeh/core/has_props.py
@@ -234,6 +234,8 @@ class HasProps(metaclass=MetaHasProps):
     ''' Base class for all class types that have Bokeh properties.
 
     '''
+    _initialized: bool = False
+
     def __init__(self, **properties):
         '''
 
@@ -246,6 +248,8 @@ def __init__(self, **properties):
         for name, value in properties.items():
             setattr(self, name, value)
 
+        self._initialized = True
+
     def __setattr__(self, name, value):
         ''' Intercept attribute setting on HasProps in order to special case
         a few situations:
diff --git a/bokeh/document/document.py b/bokeh/document/document.py
--- a/bokeh/document/document.py
+++ b/bokeh/document/document.py
@@ -381,12 +381,7 @@ def apply_json_patch(self, patch, setter=None):
         '''
         references_json = patch['references']
         events_json = patch['events']
-        references = instantiate_references_json(references_json)
-
-        # Use our existing model instances whenever we have them
-        for obj in references.values():
-            if obj.id in self._all_models:
-                references[obj.id] = self._all_models[obj.id]
+        references = instantiate_references_json(references_json, self._all_models)
 
         # The model being changed isn't always in references so add it in
         for event_json in events_json:
@@ -557,7 +552,7 @@ def from_json(cls, json):
         root_ids = roots_json['root_ids']
         references_json = roots_json['references']
 
-        references = instantiate_references_json(references_json)
+        references = instantiate_references_json(references_json, {})
         initialize_references_json(references_json, references)
 
         doc = Document()
diff --git a/bokeh/document/util.py b/bokeh/document/util.py
--- a/bokeh/document/util.py
+++ b/bokeh/document/util.py
@@ -19,6 +19,9 @@
 # Imports
 #-----------------------------------------------------------------------------
 
+# Standard library imports
+from typing import Dict
+
 # Bokeh imports
 from ..core.has_props import HasProps
 from ..model import get_class
@@ -79,11 +82,12 @@ def initialize_references_json(references_json, references, setter=None):
         # We want to avoid any Model specific initialization that happens with
         # Slider(...) when reconstituting from JSON, but we do need to perform
         # general HasProps machinery that sets properties, so call it explicitly
-        HasProps.__init__(instance)
+        if not instance._initialized:
+            HasProps.__init__(instance)
 
         instance.update_from_json(obj_attrs, models=references, setter=setter)
 
-def instantiate_references_json(references_json):
+def instantiate_references_json(references_json, existing_instances: Dict[str, HasProps]):
     ''' Given a JSON representation of all the models in a graph, return a
     dict of new model objects.
 
@@ -102,11 +106,14 @@ def instantiate_references_json(references_json):
         obj_id = obj['id']
         obj_type = obj.get('subtype', obj['type'])
 
-        cls = get_class(obj_type)
-        instance = cls.__new__(cls, id=obj_id)
-        if instance is None:
-            raise RuntimeError('Error loading model from JSON (type: %s, id: %s)' % (obj_type, obj_id))
-        references[instance.id] = instance
+        if obj_id in existing_instances:
+            references[obj_id] = existing_instances[obj_id]
+        else:
+            cls = get_class(obj_type)
+            instance = cls.__new__(cls, id=obj_id)
+            if instance is None:
+                raise RuntimeError(f"Error loading model from JSON (type: {obj_type}, id: {obj_id})")
+            references[instance.id] = instance
 
     return references
 


Test patch: