diff --git a/dash/dash-renderer/src/observers/executedCallbacks.ts b/dash/dash-renderer/src/observers/executedCallbacks.ts index 11fdcd023c..32514f67ba 100644 --- a/dash/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash/dash-renderer/src/observers/executedCallbacks.ts @@ -321,7 +321,7 @@ const observer: IStoreObserverDefinition = { childrenPropPath, appliedProps ); - if (!children) { + if (children === undefined) { return; } diff --git a/tests/integration/callbacks/test_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 7ce0296bc4..602635e25d 100644 --- a/tests/integration/callbacks/test_layout_paths_with_callbacks.py +++ b/tests/integration/callbacks/test_layout_paths_with_callbacks.py @@ -1,6 +1,9 @@ import os import json from multiprocessing import Value +import time + +import pytest from dash import Dash, Input, Output, dcc, html import dash.testing.wait as wait @@ -248,3 +251,58 @@ def check_call_counts(chapters, count): ) check_chapter("chapter1") snapshot(name="chapter-1-again") + + +@pytest.mark.parametrize( + "replacement", [None, "", 0], ids=["none", "empty-string", "zero"] +) +def test_cblp002_callback_replaces_children_with_falsy_value_and_can_remount( + dash_duo, replacement +): + view_calls = [] + + app = Dash(__name__, suppress_callback_exceptions=True) + app.layout = html.Div( + [ + html.Button("Toggle", id="toggle"), + dcc.Interval(id="tick", interval=200), + html.Div(id="container"), + ] + ) + + @app.callback( + Output("container", "children"), + Input("toggle", "n_clicks"), + prevent_initial_call=True, + ) + def toggle_view(n_clicks): + if n_clicks % 2: + return html.Div(html.Span(id="view-child"), id="view") + return replacement + + @app.callback(Output("view-child", "children"), Input("tick", "n_intervals")) + def update_view(n_intervals): + view_calls.append(n_intervals) + return f"tick {n_intervals}" + + dash_duo.start_server(app) + + dash_duo.find_element("#toggle").click() + dash_duo.wait_for_element("#view-child") + wait.until(lambda: "view-child" in dash_duo.redux_state_paths["strs"], timeout=3) + dash_duo.wait_for_contains_text("#view-child", "tick") + wait.until(lambda: view_calls, timeout=3) + + dash_duo.find_element("#toggle").click() + dash_duo.wait_for_no_elements("#view-child") + wait.until( + lambda: "view-child" not in dash_duo.redux_state_paths["strs"], timeout=3 + ) + calls_after_unmount = len(view_calls) + time.sleep(0.5) + assert len(view_calls) == calls_after_unmount + + dash_duo.find_element("#toggle").click() + dash_duo.wait_for_element("#view-child") + wait.until(lambda: "view-child" in dash_duo.redux_state_paths["strs"], timeout=3) + dash_duo.wait_for_contains_text("#view-child", "tick")