Skip to content

Commit 491c05a

Browse files
authored
Fix unmount (#901)
* remove upper bound * revert #886 + add test + misc * changelog entry * changes for latest black
1 parent cc723de commit 491c05a

File tree

14 files changed

+56
-41
lines changed

14 files changed

+56
-41
lines changed

docs/source/_exts/idom_example.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919

2020
class WidgetExample(SphinxDirective):
21-
2221
has_content = False
2322
required_arguments = 1
2423
_next_id = 0

docs/source/_exts/idom_view.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414

1515
class IteractiveWidget(SphinxDirective):
16-
1716
has_content = False
1817
required_arguments = 1
1918
_next_id = 0

docs/source/about/changelog.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
2323
Unreleased
2424
----------
2525

26-
No changes.
26+
**Reverted**
27+
28+
- :pull:`901` - reverts :pull:`886` due to :issue:`896`
29+
30+
**Fixed**
31+
32+
- :issue:`896` - Stale event handlers after disconnect/reconnect cycle
2733

2834

2935
v1.0.0-a1

requirements/pkg-deps.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ requests >=2
77
colorlog >=6
88
asgiref >=3
99
lxml >=4
10-
click >=8, <9
10+
click >=8

src/client/packages/idom-client-react/src/mount.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ function mountLayoutWithReconnectingWebSocket(
3838
socket.onopen = (event) => {
3939
console.info(`IDOM WebSocket connected.`);
4040

41+
if (mountState.everMounted) {
42+
ReactDOM.unmountComponentAtNode(element);
43+
}
4144
_resetOpenMountState(mountState);
4245

4346
mountLayout(element, {

src/idom/backend/tornado.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ def _setup_single_view_dispatcher_route(
158158

159159

160160
class IndexHandler(RequestHandler):
161-
162161
_index_html: str
163162

164163
def initialize(self, index_html: str) -> None:

src/idom/core/hooks.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def use_state(initial_value: _Type | Callable[[], _Type]) -> State[_Type]:
7575

7676

7777
class _CurrentState(Generic[_Type]):
78-
7978
__slots__ = "value", "dispatch"
8079

8180
def __init__(
@@ -148,11 +147,9 @@ def use_effect(
148147
last_clean_callback: Ref[Optional[_EffectCleanFunc]] = use_ref(None)
149148

150149
def add_effect(function: _EffectApplyFunc) -> None:
151-
152150
if not asyncio.iscoroutinefunction(function):
153151
sync_function = cast(_SyncEffectFunc, function)
154152
else:
155-
156153
async_function = cast(_AsyncEffectFunc, function)
157154

158155
def sync_function() -> Optional[_EffectCleanFunc]:

src/idom/core/layout.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,6 @@ class _LifeCycleState(NamedTuple):
651651

652652

653653
class _ThreadSafeQueue(Generic[_Type]):
654-
655654
__slots__ = "_loop", "_queue", "_pending"
656655

657656
def __init__(self) -> None:

src/idom/core/vdom.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ def separate_attributes_and_event_handlers(
258258
separated_handlers: DefaultDict[str, list[EventHandlerType]] = DefaultDict(list)
259259

260260
for k, v in attributes.items():
261-
262261
handler: EventHandlerType
263262

264263
if callable(v):

src/idom/testing/common.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async def until(
5656
condition: Callable[[_R], bool],
5757
timeout: float = IDOM_TESTING_DEFAULT_TIMEOUT.current,
5858
delay: float = _DEFAULT_POLL_DELAY,
59+
description: str = "condition to be true",
5960
) -> None:
6061
"""Check that the coroutines result meets a condition within the timeout"""
6162
started_at = time.time()
@@ -66,7 +67,7 @@ async def until(
6667
break
6768
elif (time.time() - started_at) > timeout: # pragma: no cover
6869
raise TimeoutError(
69-
f"Condition not met within {timeout} "
70+
f"Expected {description} after {timeout} "
7071
f"seconds - last value was {result!r}"
7172
)
7273

@@ -77,7 +78,12 @@ async def until_is(
7778
delay: float = _DEFAULT_POLL_DELAY,
7879
) -> None:
7980
"""Wait until the result is identical to the given value"""
80-
return await self.until(lambda left: left is right, timeout, delay)
81+
return await self.until(
82+
lambda left: left is right,
83+
timeout,
84+
delay,
85+
f"value to be identical to {right!r}",
86+
)
8187

8288
async def until_equals(
8389
self,
@@ -86,7 +92,12 @@ async def until_equals(
8692
delay: float = _DEFAULT_POLL_DELAY,
8793
) -> None:
8894
"""Wait until the result is equal to the given value"""
89-
return await self.until(lambda left: left == right, timeout, delay)
95+
return await self.until(
96+
lambda left: left == right,
97+
timeout,
98+
delay,
99+
f"value to equal {right!r}",
100+
)
90101

91102

92103
class HookCatcher:

tests/test_client.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import idom
88
from idom.backend.utils import find_available_port
9-
from idom.testing import BackendFixture, DisplayFixture
9+
from idom.testing import BackendFixture, DisplayFixture, poll
1010
from tests.tooling.common import DEFAULT_TYPE_DELAY
11+
from tests.tooling.hooks import use_counter
1112

1213

1314
JS_DIR = Path(__file__).parent / "js"
@@ -21,45 +22,53 @@ async def test_automatic_reconnect(browser: Browser):
2122
page.set_default_timeout(10000)
2223

2324
@idom.component
24-
def OldComponent():
25-
return idom.html.p("old", id="old-component")
25+
def SomeComponent():
26+
count, incr_count = use_counter(0)
27+
return idom.html._(
28+
idom.html.p("count", count, data_count=count, id="count"),
29+
idom.html.button("incr", on_click=lambda e: incr_count(), id="incr"),
30+
)
2631

2732
async with AsyncExitStack() as exit_stack:
2833
server = await exit_stack.enter_async_context(BackendFixture(port=port))
2934
display = await exit_stack.enter_async_context(
3035
DisplayFixture(server, driver=page)
3136
)
3237

33-
await display.show(OldComponent)
38+
await display.show(SomeComponent)
3439

35-
# ensure the element is displayed before stopping the server
36-
await page.wait_for_selector("#old-component")
37-
38-
# the server is disconnected but the last view state is still shown
39-
await page.wait_for_selector("#old-component")
40+
count = await page.wait_for_selector("#count")
41+
incr = await page.wait_for_selector("#incr")
4042

41-
set_state = idom.Ref(None)
43+
for i in range(3):
44+
assert (await count.get_attribute("data-count")) == str(i)
45+
await incr.click()
4246

43-
@idom.component
44-
def NewComponent():
45-
state, set_state.current = idom.hooks.use_state(0)
46-
return idom.html.p(f"new-{state}", id=f"new-component-{state}")
47+
# the server is disconnected but the last view state is still shown
48+
await page.wait_for_selector("#count")
4749

4850
async with AsyncExitStack() as exit_stack:
4951
server = await exit_stack.enter_async_context(BackendFixture(port=port))
5052
display = await exit_stack.enter_async_context(
5153
DisplayFixture(server, driver=page)
5254
)
5355

54-
await display.show(NewComponent)
56+
# use mount instead of show to avoid a page refesh
57+
display.backend.mount(SomeComponent)
58+
59+
async def get_count():
60+
# need to refetch element because may unmount on reconnect
61+
count = await page.wait_for_selector("#count")
62+
return await count.get_attribute("data-count")
63+
64+
for i in range(3):
65+
# it may take a moment for the websocket to reconnect so need to poll
66+
await poll(get_count).until_equals(str(i))
5567

56-
# Note the lack of a page refresh before looking up this new component. The
57-
# client should attempt to reconnect and display the new view automatically.
58-
await page.wait_for_selector("#new-component-0")
68+
# need to refetch element because may unmount on reconnect
69+
incr = await page.wait_for_selector("#incr")
5970

60-
# check that we can resume normal operation
61-
set_state.current(1)
62-
await page.wait_for_selector("#new-component-1")
71+
await incr.click()
6372

6473

6574
async def test_style_can_be_changed(display: DisplayFixture):

tests/test_core/test_hooks.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,6 @@ def SomeComponent():
10571057
return idom.html.div()
10581058

10591059
async with idom.Layout(SomeComponent()) as layout:
1060-
10611060
with assert_idom_did_log(r"SomeComponent\(.*?\) message is 'hello'"):
10621061
await layout.render()
10631062

@@ -1085,7 +1084,6 @@ def SomeComponent():
10851084
return idom.html.div()
10861085

10871086
async with idom.Layout(SomeComponent()) as layout:
1088-
10891087
with assert_idom_did_log(r"SomeComponent\(.*?\) message is 'hello'"):
10901088
await layout.render()
10911089

@@ -1111,7 +1109,6 @@ def SomeComponent():
11111109
return idom.html.div()
11121110

11131111
async with idom.Layout(SomeComponent()) as layout:
1114-
11151112
with assert_idom_did_not_log(r"SomeComponent\(.*?\) message is 'hello'"):
11161113
await layout.render()
11171114

tests/test_core/test_layout.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ def BadChild():
174174
raise ValueError("error from bad child")
175175

176176
with assert_idom_did_log(match_error="error from bad child"):
177-
178177
async with idom.Layout(Main()) as layout:
179178
assert (await layout.render()) == update_message(
180179
path="",
@@ -225,7 +224,6 @@ def BadChild():
225224
raise ValueError("error from bad child")
226225

227226
with assert_idom_did_log(match_error="error from bad child"):
228-
229227
async with idom.Layout(Main()) as layout:
230228
assert (await layout.render()) == update_message(
231229
path="",
@@ -754,7 +752,6 @@ def raise_error():
754752
return idom.html.button(on_click=raise_error)
755753

756754
with assert_idom_did_log(match_error="bad event handler"):
757-
758755
async with idom.Layout(ComponentWithBadEventHandler()) as layout:
759756
await layout.render()
760757
event = event_message(bad_handler.target)

tests/tooling/hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ def use_toggle(init=False):
1212

1313
def use_counter(initial_value):
1414
state, set_state = use_state(initial_value)
15-
return state, lambda: set_state(state + 1)
15+
return state, lambda: set_state(lambda old: old + 1)

0 commit comments

Comments
 (0)