Skip to content

Commit 5fdb6bf

Browse files
authored
Merge pull request #177 from zenoamaro/cleaner-rerendering
Cleaner rerendering
2 parents 8988a0e + 209c30a commit 5fdb6bf

File tree

5 files changed

+128
-53
lines changed

5 files changed

+128
-53
lines changed

.jshintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"strict": true,
2020
"maxdepth": 3,
2121
"maxparams": 4,
22-
"maxcomplexity": 6,
22+
"maxcomplexity": 8,
2323
"maxlen": 100,
2424
"eqnull": true,
2525
"lastsemic": true,

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ This release supports Quill v1.0.0+. ⚠️ There are many breaking changes, so
3333
- Updated rendering of toolbar actions (@clemmy)
3434
- Improved toolbar renderChoices implementation (@zhang-z)
3535
- Fixed use of `defaultValue` in Toolbar selects
36+
- Fixed bounds validation in setEditorSelection (@wouterh)
37+
- Exposed Quill in exports (@tdg5)
38+
- Added unhook function to clean up event listeners on unmount (@alexkrolick, @jrmmnr)
39+
- Fixed documentation typos (@l3kn)
40+
- Started testing with Enzyme (@alexkrolick)
41+
- Fixed issue where changing props caused re-render artifacts (#147)
3642

3743
v0.4.1
3844
------

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ Thanks to @clemmy and @alexkrolick for landing this much-awaited change. There a
3232

3333
---
3434

35-
🎧 **Latest published package version: `v1.0.0-beta-3`**
35+
🎧 **Latest published package version: `v1.0.0-beta-4`**
3636
Follow React Quill's development on the beta channel leading to `v1.0.0`.
37-
`npm install [email protected]3`
37+
`npm install [email protected]4`
3838

3939
---
4040

@@ -635,6 +635,7 @@ This release adds support for Quill v1.0.0+. ⚠️ There are many breaking chan
635635
- Added unhook function to clean up event listeners on unmount (@alexkrolick, @jrmmnr)
636636
- Fixed documentation typos (@l3kn)
637637
- Started testing with Enzyme (@alexkrolick)
638+
- Fixed issue where changing props caused re-render artifacts (#147)
638639

639640
#### v0.4.1
640641
- Added contents of `dist` to NPM package.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-quill",
3-
"version": "1.0.0-beta-3",
3+
"version": "1.0.0-beta-4",
44
"description": "The Quill rich-text editor as a React component.",
55
"author": "zenoamaro <[email protected]>",
66
"homepage": "https://github.com/zenoamaro/react-quill",

src/component.js

Lines changed: 117 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ var QuillComponent = React.createClass({
4242
) return new Error(
4343
'Since v1.0.0, React Quill will not create a custom toolbar for you ' +
4444
'anymore. Create a toolbar explictly, or let Quill create one. ' +
45-
'See: https://github.com/zenoamaro/react-quill#upgrading-to-react-quill-v1-0-0'
45+
'See: https://github.com/zenoamaro/react-quill#upgrading-to-react-quill-v100'
4646
);
4747
},
4848

4949
toolbar: function(props) {
5050
if ('toolbar' in props) return new Error(
5151
'The `toolbar` prop has been deprecated. Use `modules.toolbar` instead. ' +
52-
'See: https://github.com/zenoamaro/react-quill#upgrading-to-react-quill-v1-0-0'
52+
'See: https://github.com/zenoamaro/react-quill#upgrading-to-react-quill-v100'
5353
);
5454
},
5555

@@ -94,105 +94,156 @@ var QuillComponent = React.createClass({
9494
},
9595

9696
/*
97-
Changing one of these props should cause a re-render.
97+
Changing one of these props should cause a full re-render.
9898
*/
9999
dirtyProps: [
100-
'id',
101-
'className',
102-
'style',
103100
'modules',
104101
'formats',
105102
'bounds',
106103
'theme',
107104
'children',
108105
],
109106

107+
/*
108+
Changing one of these props should cause a regular update.
109+
*/
110+
cleanProps: [
111+
'id',
112+
'className',
113+
'style',
114+
'placeholder',
115+
'onKeyPress',
116+
'onKeyDown',
117+
'onKeyUp',
118+
'onChange',
119+
'onChangeSelection',
120+
],
121+
110122
getDefaultProps: function() {
111123
return {
112-
className: '',
113124
theme: 'snow',
114125
modules: {},
115126
};
116127
},
117128

118129
/*
119-
We consider the component to be controlled if
120-
whenever `value` is being sent in props.
130+
We consider the component to be controlled if `value` is being sent in props.
121131
*/
122132
isControlled: function() {
123133
return 'value' in this.props;
124134
},
125135

126136
getInitialState: function() {
127137
return {
138+
generation: 0,
128139
value: this.isControlled()
129140
? this.props.value
130141
: this.props.defaultValue
131142
};
132143
},
133144

134-
componentWillReceiveProps: function(nextProps) {
145+
componentWillReceiveProps: function(nextProps, nextState) {
146+
// If we need to regenerate the component, we can avoid a detailed
147+
// in-place update step, and just let everything rerender.
148+
if (this.shouldComponentRegenerate(nextProps, nextState)) {
149+
return this.regenerate();
150+
}
151+
135152
var editor = this.editor;
153+
136154
// If the component is unmounted and mounted too quickly
137155
// an error is thrown in setEditorContents since editor is
138156
// still undefined. Must check if editor is undefined
139157
// before performing this call.
140-
if (editor) {
141-
// Update only if we've been passed a new `value`.
142-
// This leaves components using `defaultValue` alone.
143-
if ('value' in nextProps) {
144-
// NOTE: Seeing that Quill is missing a way to prevent
145-
// edits, we have to settle for a hybrid between
146-
// controlled and uncontrolled mode. We can't prevent
147-
// the change, but we'll still override content
148-
// whenever `value` differs from current state.
149-
if (nextProps.value !== this.getEditorContents()) {
150-
this.setEditorContents(editor, nextProps.value);
151-
}
158+
if (!editor) return;
159+
160+
// Update only if we've been passed a new `value`.
161+
// This leaves components using `defaultValue` alone.
162+
if ('value' in nextProps) {
163+
// NOTE: Seeing that Quill is missing a way to prevent
164+
// edits, we have to settle for a hybrid between
165+
// controlled and uncontrolled mode. We can't prevent
166+
// the change, but we'll still override content
167+
// whenever `value` differs from current state.
168+
if (nextProps.value !== this.getEditorContents()) {
169+
this.setEditorContents(editor, nextProps.value);
152170
}
153-
// We can update readOnly state in-place.
154-
if ('readOnly' in nextProps) {
155-
if (nextProps.readOnly !== this.props.readOnly) {
156-
this.setEditorReadOnly(editor, nextProps.readOnly);
157-
}
171+
}
172+
173+
// We can update readOnly state in-place.
174+
if ('readOnly' in nextProps) {
175+
if (nextProps.readOnly !== this.props.readOnly) {
176+
this.setEditorReadOnly(editor, nextProps.readOnly);
158177
}
159178
}
160179
},
161180

162181
componentDidMount: function() {
163-
var editor = this.createEditor(
182+
this.editor = this.createEditor(
164183
this.getEditingArea(),
165184
this.getEditorConfig()
166185
);
167-
this.editor = editor;
186+
// Restore editor from Quill's native formats in regeneration scenario
187+
if (this.quillDelta) {
188+
this.editor.setContents(this.quillDelta);
189+
this.editor.setSelection(this.quillSelection);
190+
this.editor.focus();
191+
this.quillDelta = this.quillSelection = null;
192+
return;
193+
}
194+
if (this.state.value) {
195+
this.setEditorContents(this.editor, this.state.value);
196+
return;
197+
}
168198
},
169199

170200
componentWillUnmount: function() {
171-
// NOTE: Don't set the state to null here
172-
// as it would generate a loop.
173-
var e = this.getEditor();
174-
if (e) this.unhookEditor(e);
201+
var editor; if ((editor = this.getEditor())) {
202+
this.unhookEditor(editor);
203+
this.editor = null;
204+
}
175205
},
176206

177207
shouldComponentUpdate: function(nextProps, nextState) {
178-
// Rerender whenever a "dirtyProp" changes
179-
var props = this.props;
208+
var self = this;
209+
210+
// If the component has been regenerated, we already know we should update.
211+
if (this.state.generation !== nextState.generation) {
212+
return true;
213+
}
214+
215+
// Compare props that require React updating the DOM.
216+
return some(this.cleanProps, function(prop) {
217+
// Note that `isEqual` compares deeply, making it safe to perform
218+
// non-immutable updates, at the cost of performance.
219+
return !isEqual(nextProps[prop], self.props[prop]);
220+
});
221+
},
222+
223+
shouldComponentRegenerate: function(nextProps, nextState) {
224+
var self = this;
225+
// Whenever a `dirtyProp` changes, the editor needs reinstantiation.
180226
return some(this.dirtyProps, function(prop) {
181-
return !isEqual(nextProps[prop], props[prop]);
227+
// Note that `isEqual` compares deeply, making it safe to perform
228+
// non-immutable updates, at the cost of performance.
229+
return !isEqual(nextProps[prop], self.props[prop]);
182230
});
183231
},
184232

185233
/*
186-
If for whatever reason we are rendering again,
187-
we should tear down the editor and bring it up
188-
again.
234+
If we could not update settings from the new props in-place, we have to tear
235+
down everything and re-render from scratch.
189236
*/
190-
componentWillUpdate: function() {
191-
this.componentWillUnmount();
237+
componentWillUpdate: function(nextProps, nextState) {
238+
if (this.state.generation !== nextState.generation) {
239+
this.componentWillUnmount();
240+
}
192241
},
193242

194-
componentDidUpdate: function() {
195-
this.componentDidMount();
243+
componentDidUpdate: function(prevProps, prevState) {
244+
if (this.state.generation !== prevState.generation) {
245+
this.componentDidMount();
246+
}
196247
},
197248

198249
getEditorConfig: function() {
@@ -222,6 +273,19 @@ var QuillComponent = React.createClass({
222273
return this.state.selection;
223274
},
224275

276+
/*
277+
Regenerating the editor will cause the whole tree, including the container,
278+
to be cleaned up and re-rendered from scratch.
279+
*/
280+
regenerate: function() {
281+
// Cache selection and contents in Quill's native format to be restored later
282+
this.quillDelta = this.editor.getContents();
283+
this.quillSelection = this.editor.getSelection();
284+
this.setState({
285+
generation: this.state.generation + 1,
286+
});
287+
},
288+
225289
/*
226290
Renders an editor area, unless it has been provided one to clone.
227291
*/
@@ -230,22 +294,26 @@ var QuillComponent = React.createClass({
230294
var children = this.props.children;
231295

232296
var properties = {
297+
key: this.state.generation,
233298
ref: function(element) { self.editingArea = element },
234-
dangerouslySetInnerHTML: { __html:this.getEditorContents() }
235299
};
236300

237-
if (React.Children.count(children) === 0) {
238-
return React.DOM.div(properties);
239-
}
301+
var customElement = React.Children.count(children)
302+
? React.Children.only(children)
303+
: null;
304+
305+
var editingArea = customElement
306+
? React.cloneElement(customElement, properties)
307+
: React.DOM.div(properties);
240308

241-
var editor = React.Children.only(children);
242-
return React.cloneElement(editor, properties);
309+
return editingArea;
243310
},
244311

245312
render: function() {
246313
return React.DOM.div({
247314
id: this.props.id,
248315
style: this.props.style,
316+
key: this.state.generation,
249317
className: ['quill'].concat(this.props.className).join(' '),
250318
onKeyPress: this.props.onKeyPress,
251319
onKeyDown: this.props.onKeyDown,

0 commit comments

Comments
 (0)