-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix CypherEditor state handling and add tests #251
Conversation
🦋 Changeset detectedLatest commit: c211e25 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
2464500
to
acae14e
Compare
fdfe091
to
4cad3ae
Compare
68f2f1f
to
7b46c79
Compare
4cad3ae
to
434c486
Compare
7b46c79
to
1eb8076
Compare
33ef1cd
to
2644415
Compare
Unit tests are sometimes failing with vitest-dev/vitest#6131 UPDATE: Fixed by restricting the number of workers to 1. I think this is an acceptable workaround given the small number of unit tests in the react-codemirror package. |
Of the two options I think the first one makes most sense. If you want to investigate the second option here's some context: I added the debounce here, along with a few other changes to prevent what was before heavy input lag. In your linked codemirror doc we can see that autocompletion calls are already debounced-ish with 100ms, and as I remember it when I profiled it the input lag it was the frequent react re-renders that were problematic. My suggestion would be to profile / compare editing the A third option that I think you said @daveajrussell originally suggested could also be interesting: don't support changing the value directly when using a controlled component. Consumers will have to chose between an uncontrolled component where they need to read and set the value manually via the |
0cae0f1
to
756a932
Compare
Agree.
I'm not sure I'm ready to go down another rabbit hole of performance profiling and optimization. 😄
Instinctively I don't think this solves problem 3. In theory, even if we disallow changing the value through the ref, that's still functionally identical to simply typing quickly and thus the same problems should exist. |
@danieladugyan was the problem with the lastDispatchedValue fix not that it didn't properly handle the external setting from |
I'm honestly not sure. However, I discounted Furthermore, even if |
756a932
to
ecd7bad
Compare
Not necessarily, you could change the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good from my POV
- Allows it to be imported from Playwright tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested out in my upx repo, looks good 👍
This PR once again attempts to fix the issues with how the
CypherEditor
handles state updates. I'll try to explain below what the problem is and how we could solve it.TL;DR the
CypherEditor
now flushes debounced changes beforeonExecute
, and handlesthis.prop.value
updates by checking ifthis.props.value
is different fromprevProps.value
.Problems
Problem 1: value not cleared onExecute
CypherEditor
component as follows:"foo"
and then immediately press Enter. This must be done quickly; the component debounces itsonChange
calls with a 200 ms timer. This meansonChange
is called 200 ms after user stops typing instead of being called on every keystroke.setValue('')
which of course causes the component to re-render with an empty string asvalue
. However, there's still anonChange
call waiting to happen, and when it fires thevalue
will be set to"foo"
again.Solution 1
There's a relatively straightforward solution to this problem. After
setValue('')
is called, the component'scomponentDidUpdate()
method will be called. Here, we can simply check if the value has changed, and it if it has, cancel the debouncedonChange
call. This ensure we can't accidentally override asetValue('')
call with pending inputs.Problem 2: programmatic input not working
CypherEditor
component again."foo"
and then immediately re-render the component. Why? Well, this happens in practice sometimes. For instance, another component might programmatically input a value (i.e. usingsetValueAndFocus("foo")
) and subsequently cause a re-render.componentDidUpdate()
method will be called. We'll once again reach the code below:value
is lagging behind theeditorText
and the component is re-rendering following an edit to theeditorText
, as opposed to throughsetValue()
. In this situation we must not cancel the debouncedonChange
. If we do, the input will just stay empty.Solution 2
Instead of checking whether
this.props.value
is different from theeditorText
, we can check whether it has changed from the previous props. If the component is just re-rendering, the value will be unchanged and we'll know not to cancel the pendingonChange
call.Problem 3: value still not cleared onExecute
However, the component is still not working as expected. The first problem is actually a bit of an edge case. The component's value was empty initially, then we typed some input and immediately pressed Enter. If we're quick enough, both
this.props.value
andprevProps.value
will be''
! According to our logic, this means thatsetValue('')
has not been called, but rather the component is just re-rendering. Therefore, we must conclude that there is no way to distinguish asetValue()
call from a re-render, at least in some cases.Conclusion
Option 1
As far as I can tell there is no way to address the underlying problem with having a debounced
onChange
function. WhencomponentDidUpdate
is called, we don't know whether to cancel upcomingonChange
calls or not. However, in practice, problematicsetValue
calls only seem originate from theonExecute
callback. For most practical applications, we can therefore solve the problem by flushing upcomingonChange
calls before callingonExecute
. This ensures that thesetValue
calls happen in their expected order. However, note that the component will continue to overridesetValue
calls originating from outsideonExecute
(e.g if while the user is typing setValue('foo') is fired for some reason, then the value will be reset to whatever the user was typing). See the tests that have been annotated withtest.fails
for concrete examples.Option 2
The ideal solution would be to remove the debounce entirely. Instead, we can just call
onChange
immediately. However, I expect this will lead to performance problems. It would therefore be interesting to attempt to move the debounce logic to the specific method calls causing the performance problems (e.g. autocomplete, syntax highlighting etc?). Instead of debouncing theonChange
call, we would instead be debouncing the slow methods themselves.Flowchart
Further reading
Debouncing and Throttling Explained Through Examples
https://codemirror.net/docs/ref/#autocomplete.autocompletion%5Econfig.activateOnTypingDelay