Repository: DevExpress/devextreme-react
Pull request #272 (2018-12-27 11:35:35)

Improve performance when multiple templates are used (T701352) (#269)


Patch:
diff --git a/example/list-example.tsx b/example/list-example.tsx
index 5a5e445..9262669 100644
--- a/example/list-example.tsx
+++ b/example/list-example.tsx
@@ -42,6 +42,12 @@ function ItemKeyGetter(data: any) {
     return data.text;
 }
 
+function ItemsRender(item: string) {
+    return <i>{item}</i>;
+}
+
+const listItems: string[] = ["orange", "apple", "potato"];
+
 // tslint:disable-next-line:max-classes-per-file
 export default class extends React.Component<any, { text: string; items: IListItemProps[]; }> {
 
@@ -77,8 +83,8 @@ export default class extends React.Component<any, { text: string; items: IListIt
                 <hr />
                 <h4>List with function template</h4>
                 <List
-                    items={["orange", "apple", "potato"]}
-                    itemRender={(item: string) => <i>{item}</i>}
+                    items={listItems}
+                    itemRender={ItemsRender}
                 />
                 <hr />
                 <h4>List with component template</h4>
diff --git a/src/core/component-base.ts b/src/core/component-base.ts
index 6327f6d..53d52c5 100644
--- a/src/core/component-base.ts
+++ b/src/core/component-base.ts
@@ -33,6 +33,9 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   protected readonly _templateProps: ITemplateMeta[] = [];
   protected readonly _expectedChildren: Record<string, INestedOption>;
 
+  private _templateCallbacks: any[] = [];
+  private _templateTimeout: any;
+
   private readonly _templateHost: TemplateHost;
   private readonly _optionsManager: OptionsManager;
 
@@ -70,10 +73,12 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   }
 
   public componentWillUnmount() {
+    this._clearTemplates();
     if (this._instance) {
       events.triggerHandler(this._element, DX_REMOVE_EVENT);
       this._instance.dispose();
     }
+    this._optionsManager.dispose();
   }
 
   protected _prepareChildren(args: any[] = []): any[] {
@@ -125,15 +130,30 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   }
 
   private _updateTemplatesState(callback: any) {
-    this.setState((state: IState) => {
+    this._templateCallbacks.push(callback);
+
+    if (this._templateTimeout) { return; }
+
+    this._templateTimeout = setTimeout(() => {
+      this.setState((state: IState) => {
         const templates = { ...state.templates };
-        callback(templates);
-        return {
-            templates
-        };
+        for (const cb of this._templateCallbacks) {
+          cb(templates);
+        }
+        this._clearTemplates();
+        return { templates };
+      });
     });
   }
 
+  private _clearTemplates() {
+    this._templateCallbacks.length = 0;
+    if (this._templateTimeout) {
+      clearTimeout(this._templateTimeout);
+      this._templateTimeout = undefined;
+    }
+  }
+
   private _getElementProps(): Record<string, any> {
     const elementProps: Record<string, any> = {
       ref: (element: HTMLDivElement) => this._element = element
diff --git a/src/core/options-manager.ts b/src/core/options-manager.ts
index d098fd1..eb97e90 100644
--- a/src/core/options-manager.ts
+++ b/src/core/options-manager.ts
@@ -153,6 +153,13 @@ class OptionsManager {
         return this._registerNestedOption(component, expectedChildren, this._nestedOptions);
     }
 
+    public dispose() {
+        for (const optionName of Object.keys(this._guards)) {
+            window.clearTimeout(this._guards[optionName]);
+            delete this._guards[optionName];
+        }
+    }
+
     private _setOption(name: string, value: any): void {
         let actualValue = value;
         if (isEventHanlder(name, value)) {
diff --git a/src/core/template-wrapper.ts b/src/core/template-wrapper.ts
index b338452..9f96f1e 100644
--- a/src/core/template-wrapper.ts
+++ b/src/core/template-wrapper.ts
@@ -24,8 +24,9 @@ class TemplateWrapper extends React.PureComponent<ITemplateWrapperProps> {
                     style: { display: "none" },
                     ref: (element: HTMLDivElement) => this._removalListener = element
                 }),
-            )
-        , this.props.container);
+            ),
+            this.props.container
+        );
     }
 
     public componentDidMount() {
@@ -33,19 +34,21 @@ class TemplateWrapper extends React.PureComponent<ITemplateWrapperProps> {
             const onRendered: () => void = this.props.onRendered;
             setTimeout(() => onRendered());
         }
-        const restoreRemovedContent = () => {
-            // Let React remove it itself
-            const node = ReactDOM.findDOMNode(this);
-            if (node) {
-                this.props.container.appendChild(node);
-            }
-            this.props.container.appendChild(this._removalListener);
-        };
+
         events.one(this._removalListener, DX_REMOVE_EVENT, () => {
-            restoreRemovedContent();
+            this._restoreRemovedContent.call(this);
             this.props.onRemoved();
         });
     }
+
+    private _restoreRemovedContent() {
+        // Let React remove it itself
+        const node = ReactDOM.findDOMNode(this);
+        if (node) {
+            this.props.container.appendChild(node);
+        }
+        this.props.container.appendChild(this._removalListener);
+    }
 }
 
 export {


Test patch:
diff --git a/src/core/__tests__/component.test.tsx b/src/core/__tests__/component.test.tsx
index 4f8c1e1..07f724d 100644
--- a/src/core/__tests__/component.test.tsx
+++ b/src/core/__tests__/component.test.tsx
@@ -1,7 +1,7 @@
 import * as events from "devextreme/events";
 
 import { mount, React, shallow } from "./setup";
-import { TestComponent, Widget, WidgetClass } from "./test-component";
+import { fireOptionChange, TestComponent, Widget, WidgetClass } from "./test-component";
 
 describe("rendering", () => {
 
@@ -161,4 +161,16 @@ describe("disposing", () => {
         expect(handleDxRemove).toHaveBeenCalledTimes(1);
     });
 
+    it("remove option guards", () => {
+        const component = shallow(
+            <TestComponent option1={true} />
+        );
+
+        fireOptionChange("option1", false);
+        component.unmount();
+        jest.runAllTimers();
+
+        expect(Widget.option.mock.calls.length).toBe(0);
+    });
+
 });
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index f37496e..ebb1045 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -24,9 +24,11 @@ function renderTemplate(name: string, model?: any, container?: any, onRendered?:
     model = model || {};
     container = container || document.createElement("div");
     const render = WidgetClass.mock.calls[0][1].integrationOptions.templates[name].render;
-    return render({
+    const result = render({
         container, model, onRendered
     });
+    jest.runAllTimers();
+    return result;
 }
 
 function renderItemTemplate(model?: any, container?: any, onRendered?: () => void): Element {
@@ -217,6 +219,7 @@ function testTemplateOption(testedOption: string) {
         parentElement.removeChild(removeListener);
         parentElement.removeChild(templateContent);
         events.triggerHandler(removeListener, "dxremove");
+        jest.runAllTimers();
         component.update();
         expect(Object.getOwnPropertyNames(component.state("templates")).length).toBe(0);
     });