-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Upgrade to React v16 lifecycle #1975
Upgrade to React v16 lifecycle #1975
Conversation
// the updates, then warn the user that they may be doing something wrong. | ||
if (this.tmp.resolves > 5 && this.tmp.resolves == this.tmp.updates) { | ||
logger.warn( | ||
'A Slate <Editor> is re-resolving `props.plugins` or `props.schema` on each update, which leads to poor performance. This is often due to passing in a new `schema` or `plugins` prop with each render by declaring them inline in your render function. Do not do this!' |
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.
how about making that === 5 so you only warn once?
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.
This warning only appears in dev mode. We want the warn appears in each update, so the developer cannot miss it.
This PR is ready for review~ |
obj[handler] = this[handler] | ||
return obj | ||
}, {}) | ||
const handlers = this.getHandlers() |
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.
im not sure why the initial implementation is the way it is, but this is actually creating new behavior (new function was being created before on every render, now its not).
are we sure this doesn't subtly change anything?
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.
Because
- We do not need to re-evaluate a new object
- We do not need to mixin a lot of onXXX functions
Therefore I use a getHandlers for this part of logic
@@ -5,6 +5,7 @@ import SlateTypes from 'slate-prop-types' | |||
import Types from 'prop-types' | |||
import logger from 'slate-dev-logger' | |||
import { Schema, Stack } from 'slate' | |||
import memoize from 'memoize-one' |
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.
do we want to call this memoizeOne
or something similar, where other parts of the codebase use other memoization strategies?
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.
I think, for react component in slate-react, we only have this memoize currently. I may change the name when the memoize strategy becomes more complex.
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.
I agree, I think calling it memoizeOne
is a good idea here.
*/ | ||
|
||
getStackWithMemoization = memoize(plugins => { | ||
this.tmp.resolves++ |
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.
should we move this.tmp.resolves++
into this.resolvePlugins
just for consistency?
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.
Yes. Thank you. Fixed~
*/ | ||
|
||
processValueOnChange = memoize((value, stack) => { | ||
if (this.tmp.change && value === this.tmp.change.value) return value |
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.
if we are going to memoize this, should we explicitly pass in this.tmp.change.value
as an argument?
or are there other protections against this.tmp.change.value
changing between calls to this method while value
and stack
stay the same?
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.
This part is to ensure processValueOnChange(processValueOnChange(value)) === processValueOnChange(value)
The reason is that, in each update, we will have stack.run('onChange', change)
passed to the parent container, then the parent container will pass stack.run('onChange', change)
back as props.value
. And I do not want to have the stack.run('onChange', change)
run again when receiving props.
I do not see a neat way to solve the problem. The only thing I can do is to ensure all value are updates are passed via processValueOnChange
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.
@jtadmor Hi, I have just removed this part of logic in recent commit, and put the value verification in didMount and onChange. I agree with you that we shall leave the memoized function pure.
Cheers,
const { onChange } = this.props | ||
if (value == this.value) return | ||
onChange(change) | ||
if (value === this.value) return |
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.
isnt this.value
just a getter that resolves to the same thing as line 269
right above? seems like this will cause it to abort on every run.
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.
Yes.
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.
Hey @zhujinxuan thanks for this. Can you explain what the goal of this is, or why it's important? Also what you mean by "removing flushChange
"?
props.text != this.props.text || | ||
props.parent != this.props.parent |
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.
What are these changes for?
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.
Because marks can be the same value with different reference. Therefore I think it is perhaps better with immutable equals
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.
Aren't these changes unrelated to the rest of this pull request though?
* @return {Stack} | ||
*/ | ||
|
||
getStackWithMemoization = memoize(plugins => { |
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.
Can you name these resolveStack/resolveSchema
to match the existing resolvePlugins
?
Why not have these call resolvePlugins
internally themselves instead of needing to take in plugins? (Might be a good reason for it, I'm just quickly reading.)
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.
Fixed~
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.
Because we need a argument to make the function working with right cache. Therefore we would like to pass the plugins. If we do not have plugins as argument, then it will always return the same value regardless of plugins change
const { onChange } = this.props | ||
if (value == this.value) return | ||
onChange(change) | ||
this.tmp.value = value |
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.
Why this?
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.
All our values are updated by this.props.onChange
and stack.run('onChange')
. During the mount, we pass the stack.run('onChange', this.props.value)
back to the onChange, and the the parent container feed use the value after onChange.
But we do not want to have onChange called again on the value that we returned to the parent container and then passed back to me. Because calling onChange again and again on the same value will dangerously lead to infinite loop.
Therefore I use a tmp.value and tell the get value() not to run onChange if it is already processed by onChange(https://github.com/ianstormtaylor/slate/pull/1975/files#diff-0f1805ffc056f7c54e6a6f98ba0259a1R174)
Hi, @ianstormtaylor . The main goal of this PR is to remove the deprecated lifecycle The React team is suggesting to use Because |
8c37241
to
eeed3d5
Compare
} | ||
}) | ||
} | ||
getHandlers = memoize(() => { |
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.
It seems like there's no reason to memoize this? Instead we could just build the this.handlers
object in the constructor
instead, and always refer to it. That would be less confusing I think. (Or maybe even class properties allow you to use this
without needing the constructor even?)
@@ -5,6 +5,7 @@ import SlateTypes from 'slate-prop-types' | |||
import Types from 'prop-types' | |||
import logger from 'slate-dev-logger' | |||
import { Schema, Stack } from 'slate' | |||
import memoize from 'memoize-one' |
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.
I agree, I think calling it memoizeOne
is a good idea here.
this.onEvent(handler, ...args) | ||
} | ||
}) | ||
tmp = { |
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.
This is a nice simplification!
this.props.onChange(change) | ||
} | ||
} | ||
getHandlers = memoize(() => |
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.
Same here for this, no reason to memoize it seems.
normalizeOnChangeValue = memoize((value, stack) => { | ||
const change = value.change() | ||
stack.run('onChange', change, this) | ||
return change.value |
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.
This doesn't make sense to me, we're losing the change.operations
here because they are not being exposed to the user. This doesn't work for collaborative editing.
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.
Oh, I see. onChange
must return a change
with all operations in this cycle. I am working on that.
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.
Oh, I see. I will use this memoize function only in get value
; As for onChange
function, we will run stack.run('onChange', change, this)
instead of calling this function.
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.
Fixed~ this memoize function will be only used in get value
if (value == this.value) return | ||
onChange(change) | ||
this.tmp.value = value | ||
onChange(value.change()) |
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.
This also doesn't make sense to return a new change object. It should return the change object with the actual operations on it.
props.text != this.props.text || | ||
props.parent != this.props.parent |
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.
Aren't these changes unrelated to the rest of this pull request though?
obj[handler] = editor[handler] | ||
return obj | ||
}, {}) | ||
const handlers = editor.getHandlers() |
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.
I like how handlers have been simplified in other parts of this pull request. But I think here it feels strange. This is probably because it's a bit weird that we're rendering <Content>
here, instead of passing it through as a child in the Editor.render
. Could you change that? Then there will be no reaching for those handlers.
this.state.stack = stack | ||
|
||
// Run `onChange` on the passed-in value because we need to ensure that it | ||
// is normalized, and queue the resulting change. |
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.
Where has this logic gone? This is important to ensure that the editor never renders a value
that is invalid according to the schema.
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.
@ianstormtaylor This part of logic has gone to get value
get value
use the resolved stack, and use the resolved stack to get value after onChange
function.
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.
Oh, I see. Do you mean if stack changes and value is unchanged, we shall still run onChange
? This is fixed in 5188877
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.
With the current logic on master
, if the <Editor>
is passed an invalid value
, it will normalize it against the schema before rendering. And then after mounting it will ensure that the operations in that extra normalization pass are propagated to onChange
so that the view layer is aware.
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.
@ianstormtaylor I see. The normalize logic is put into get value()
in https://github.com/zhujinxuan/slate/blob/ef7e626bc980f7680aec751a633e7699d7be4eee/packages/slate-react/src/components/editor.js#L173;
get value
will get value from either last this.onChange
function or associateStackAndValue
(https://github.com/zhujinxuan/slate/blob/ef7e626bc980f7680aec751a633e7699d7be4eee/packages/slate-react/src/components/editor.js#L195). Both have normalized value by stack.run('onChange)
144ded1
to
e5e670d
Compare
Hi, @ianstormtaylor . I think I am missing some of your points. Can I know what the current problem is with this PR? |
@zhujinxuan it's a bit hard to follow, but it looks better now. But I think it's still not compatible with the current behavior. Specifically this case:
It looks like now this PR does everything but 6. which is required for collaborative editing implementations to be able to know what operations were performed. It looks like it's creating a new change object instead as |
ef7e626
to
5a9c9f4
Compare
Codecov Report
@@ Coverage Diff @@
## master #1975 +/- ##
==========================================
- Coverage 66.82% 66.78% -0.04%
==========================================
Files 68 68
Lines 5624 5624
==========================================
- Hits 3758 3756 -2
- Misses 1866 1868 +2
Continue to review full report at Codecov.
|
@ianstormtaylor How about this version? I queue changes with a Map and clear the Map after each update to prevent memory leak. |
I honestly don't understand what's going on there. What's the identity function for? Why are all the changes being cached in a This isn't something I'm likely to merge if it makes the logic (which already wasn't obvious) even more confusing than before. |
@ianstormtaylor I see. I just find that using
|
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.
Hey @zhujinxuan, nice this is looking better! Few more comments inline.
} | ||
|
||
this.tmp.updates++ |
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.
Why does updates
increment here?
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.
This updates calculates the number of completed life cycles. It will be compared to tmp.resolves to decide whether to warn in componentDidUpdate that schema is re-created too many times.
It is ++ in DidMount because in the first mount, resolvePlugins will increase the tmp.resolves.
* @param {Change} change | ||
*/ | ||
|
||
queueChange(change) { |
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.
Can you remove this function and inline the logic instead since it's only used in one place now?
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.
Fixed~
* @param {Change} change | ||
*/ | ||
|
||
onChange = change => { |
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.
We can't remove this editor.onChange(change)
function because it's part of the public API that people are depending on. At least for now.
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.
Fixed. onChnage
is restored in 37502ee
c8faf84
to
3a2bc68
Compare
* @return {Change} | ||
*/ | ||
|
||
associateStackAndValue = memoizeOne((value, stack) => { |
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.
Can you call this resolveValue
to be consistent with the other places?
} | ||
|
||
get value() { | ||
return this.state.value | ||
if (this.tmp.value === this.props.value && this.tmp.stack === this.stack) { |
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.
Can you add a comment here to explain what this is doing?
debug('onChange', { change }) | ||
const { value } = change | ||
if (value == this.props.value && change.operations.size === 0) return | ||
this.stack.run('onChange', change, this) |
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.
Thanks for re-adding this onChange
method.
We've now got another issue with this logic though, which is that there are three different places that stack.run('onChange'
is called from, which I don't think is good because it makes things hard to follow.
Right now, it seems like one of the reasons for the duplication is that in the componentDidMount
we're doing this.change(change => change)
to actually flush any existing changes. This feels like too much magic, it isn't clear. And if that line was refactored to be more clear and not depend on the same code path, I think it would allow us to remove the duplcation.
For example (pseudocode):
if (this.tmp.change) {
this.flushChange()
}
Ideally editor.change
should be reduced to:
change = (...args) => {
const { value } = this
const change = value.change()
change.call(...args)
this.onChange(change)
}
Otherwise it probably means we're doing something too confusing.
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.
Hi, @ianstormtaylor . How about 3257e3e? The problem is that onChange(change)
can take an arbitrary change that not restricted to the inner state of the editor. So we have to decide whether to run stack.run(
onChange)
in difference places. One for inner change state this.tmp.change
in this.change
, the other for arbitrary change in this.onChange
* @param {Change} change | ||
*/ | ||
|
||
updateChange = change => { |
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.
Similarly, I think this updateChange
method should be only called from a single place, and then it wouldn't need to be its own method.
8173c45
to
3257e3e
Compare
8719c34
to
6ce866e
Compare
Thanks @zhujinxuan! I've added a single commit there which refactors things a bit, and cleans up some of the comments and logging. It will hopefully make the code slightly easier to follow. (Allow it will still be confusing because memoization is inherently confusing I think.) Thanks for working on this! |
* Trying to use memoization and upgrade to react v16 * Fix error * Fix error * Fix handlers error * Add annotation * Remove EventHandlers * No state * Remove un-necessary polyfill * Remove un-necessary polyfill * Remove un-necessary handlers settings * Early Return * Fix Early Return * Fix onChange * Do not run onChange stack twice on same change * Update annotation * Better sense of resolve++ * Cache value in onChange and didMount * Remove un-necessary rechack * Renaming * Remove change in leaf.js * Handlers as this.handlers * do not re-initialize change in onChange * Re-run onChange stack only when change happens * Update value when stack changes * Rename to memoize-one * queue changes * Unify interface * Fix bug * Add document * Remove id * Do not use map * Fix bug * Fix eslint * Fix update when props.value changes * Add annotation * Fix stack * Inline queueChange * Restore onChange * restore onChange * Refactor change and onChange * Use onChange as the single interface for update * Do not flushChange if inside event * Give a warning about synchronous editor.change call * Change isInChange in editor.change * refactor resolution and tmp logic, cleanup code
Is this adding or improving a feature or fixing a bug?
feature
What's the new behavior?
memoize-one
instead of state as recommended in reactJS https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoizationprops.onChange
, then no state.value is provided.props.onChange
Does this fix any issues or need any specific reviewers?
Fixes: #1426 #1427
Reviewers: @