Skip to content

Commit 857c789

Browse files
authored
fix flicker with observedValues buffer (#697)
We do this by constructing a buffer of observed input values. Then, each time the server renders, we check to see if the next rendered value matches the first value in the buffer. If it does then we continue to display the last value in the buffer and shift the buffer array. If it doesn't then we clear the buffer array and display that rendered value.
1 parent a029189 commit 857c789

File tree

2 files changed

+56
-4
lines changed

2 files changed

+56
-4
lines changed

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function UserInputElement({ model }) {
8181
// order to allow all changes committed by the user to be recorded in the order they
8282
// occur. If we don't the user may commit multiple changes before we render next
8383
// causing the content of prior changes to be overwritten by subsequent changes.
84-
const value = props.value;
84+
let value = props.value;
8585
delete props.value;
8686

8787
// Instead of controlling the value, we set it in an effect.
@@ -91,6 +91,25 @@ function UserInputElement({ model }) {
9191
}
9292
}, [ref.current, value]);
9393

94+
// Track a buffer of observed values in order to avoid flicker
95+
const observedValues = React.useState([])[0];
96+
if (observedValues) {
97+
if (value === observedValues[0]) {
98+
observedValues.shift();
99+
value = observedValues[observedValues.length - 1];
100+
} else {
101+
observedValues.length = 0;
102+
}
103+
}
104+
105+
const givenOnChange = props.onChange;
106+
if (typeof givenOnChange === "function") {
107+
props.onChange = (event) => {
108+
observedValues.push(event.target.value);
109+
givenOnChange(event);
110+
};
111+
}
112+
94113
// Use createElement here to avoid warning about variable numbers of children not
95114
// having keys. Warning about this must now be the responsibility of the server
96115
// providing the models instead of the client rendering them.

tests/test_client.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import asyncio
2+
import time
13
from pathlib import Path
24

35
import idom
46
from idom.testing import ServerMountPoint
7+
from tests.driver_utils import send_keys
58

69

710
JS_DIR = Path(__file__).parent / "js"
@@ -71,14 +74,44 @@ def ButtonWithChangingColor():
7174

7275
button = driver.find_element("id", "my-button")
7376

74-
assert get_style(button)["background-color"] == "red"
77+
assert _get_style(button)["background-color"] == "red"
7578

7679
for color in ["blue", "red"] * 2:
7780
button.click()
78-
driver_wait.until(lambda _: get_style(button)["background-color"] == color)
81+
driver_wait.until(lambda _: _get_style(button)["background-color"] == color)
7982

8083

81-
def get_style(element):
84+
def _get_style(element):
8285
items = element.get_attribute("style").split(";")
8386
pairs = [item.split(":", 1) for item in map(str.strip, items) if item]
8487
return {key.strip(): value.strip() for key, value in pairs}
88+
89+
90+
def test_slow_server_response_on_input_change(display, driver, driver_wait):
91+
"""A delay server-side could cause input values to be overwritten.
92+
93+
For more info see: https://github.com/idom-team/idom/issues/684
94+
"""
95+
96+
delay = 0.2
97+
98+
@idom.component
99+
def SomeComponent():
100+
value, set_value = idom.hooks.use_state("")
101+
102+
async def handle_change(event):
103+
await asyncio.sleep(delay)
104+
set_value(event["target"]["value"])
105+
106+
return idom.html.input({"onChange": handle_change, "id": "test-input"})
107+
108+
display(SomeComponent)
109+
110+
inp = driver.find_element("id", "test-input")
111+
112+
text = "hello"
113+
send_keys(inp, text)
114+
115+
time.sleep(delay * len(text) * 1.1)
116+
117+
driver_wait.until(lambda _: inp.get_attribute("value") == "hello")

0 commit comments

Comments
 (0)