diff --git a/package.json b/package.json
index 26f3bfd669..88cc35a18f 100644
--- a/package.json
+++ b/package.json
@@ -47,9 +47,9 @@
"extract-text-webpack-plugin": "^0.9.1",
"gh-pages": "^0.4.0",
"html": "0.0.10",
+ "immutable": "^3.7.6",
"jsdom": "^7.2.1",
"mocha": "^2.3.0",
- "react-addons-shallow-compare": "^0.14.3",
"react-addons-test-utils": "^0.14.3",
"react-codemirror": "^0.2.3",
"react-transform-catch-errors": "^1.0.0",
diff --git a/playground/app.js b/playground/app.js
index d95dc892ba..b21e76791a 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -1,12 +1,14 @@
import React, { Component } from "react";
import { render } from "react-dom";
-import shallowCompare from "react-addons-shallow-compare";
import Codemirror from "react-codemirror";
import "codemirror/mode/javascript/javascript";
+import { shouldRender } from "../src/utils";
import { samples } from "./samples";
import Form from "../src";
+import Immutable from "immutable";
+
import "codemirror/lib/codemirror.css";
import "./styles.css";
@@ -39,7 +41,7 @@ class Editor extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
onCodeChange(code) {
@@ -78,7 +80,7 @@ class Selector extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
onClick(label, sampleData, event) {
@@ -117,7 +119,7 @@ class App extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
load(data) {
@@ -126,6 +128,18 @@ class App extends Component {
_ => this.setState({...data, form: true}));
}
+ onSchemaChange(schema) {
+ this.setState({schema});
+ }
+
+ onUISchemaChange(uiSchema) {
+ this.setState({uiSchema});
+ }
+
+ onFormDataChange(formData) {
+ this.setState({formData});
+ }
+
render() {
return (
@@ -136,17 +150,17 @@ class App extends Component {
this.setState({schema})} />
+ onChange={this.onSchemaChange.bind(this)} />
this.setState({uiSchema})} />
+ onChange={this.onUISchemaChange.bind(this)} />
this.setState({formData})} />
+ onChange={this.onFormDataChange.bind(this)} />
diff --git a/src/components/Form.js b/src/components/Form.js
index 820db91096..08154bd769 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -1,10 +1,9 @@
import React, { Component, PropTypes } from "react";
import { Validator } from "jsonschema";
-import shallowCompare from "react-addons-shallow-compare";
import SchemaField from "./fields/SchemaField";
import TitleField from "./fields/TitleField";
-import { getDefaultFormState } from "../utils";
+import { getDefaultFormState, shouldRender } from "../utils";
import ErrorList from "./ErrorList";
export default class Form extends Component {
@@ -35,7 +34,7 @@ export default class Form extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
validate(formData, schema) {
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 8052e56d52..2bbeb6b7bc 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -1,11 +1,11 @@
import React, { Component, PropTypes } from "react";
-import shallowCompare from "react-addons-shallow-compare";
import {
getDefaultFormState,
isMultiSelect,
optionsList,
- retrieveSchema
+ retrieveSchema,
+ shouldRender
} from "../../utils";
import SelectWidget from "./../widgets/SelectWidget";
@@ -33,7 +33,7 @@ class ArrayField extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
get itemTitle() {
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index c780ff2239..746b4fa8a3 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -1,10 +1,10 @@
import React, { Component, PropTypes } from "react";
-import shallowCompare from "react-addons-shallow-compare";
import {
getDefaultFormState,
orderProperties,
- retrieveSchema
+ retrieveSchema,
+ shouldRender
} from "../../utils";
@@ -28,7 +28,7 @@ class ObjectField extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return shallowCompare(this, nextProps, nextState);
+ return shouldRender(this, nextProps, nextState);
}
isRequired(name) {
diff --git a/src/utils.js b/src/utils.js
index afcb08e492..9f9e880b7f 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,3 +1,5 @@
+import Immutable from "immutable";
+
import PasswordWidget from "./components/widgets/PasswordWidget";
import RadioWidget from "./components/widgets/RadioWidget";
import UpDownWidget from "./components/widgets/UpDownWidget";
@@ -186,3 +188,23 @@ export function retrieveSchema(schema, definitions={}) {
// Retrieve the referenced schema definition.
return findSchemaDefinition(schema.$ref, definitions);
}
+
+function flattenFunctions(obj) {
+ return Object.keys(obj).reduce((acc, key) => {
+ const value = obj[key];
+ if (typeof value === "function") {
+ acc[key] = value.toString();
+ } else if (value !== null && typeof value === "object") {
+ acc[key] = flattenFunctions(value);
+ } else {
+ acc[key] = value;
+ }
+ return acc;
+ }, {});
+}
+
+export function shouldRender(comp, nextProps, nextState) {
+ const immu = (obj) => Immutable.fromJS(flattenFunctions(obj));
+ return !immu(comp.props).equals(immu(nextProps)) ||
+ !immu(comp.state).equals(immu(nextState));
+}
diff --git a/test/Form_test.js b/test/Form_test.js
index 6bdb986f11..b81eea9afd 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -493,4 +493,18 @@ describe("Form", () => {
});
});
});
+
+ describe("Performances", () => {
+ it("should not render if new props are equivalent", () => {
+ const schema = {type: "string"};
+ const uiSchema = {};
+
+ const {comp} = createComponent({schema, uiSchema});
+ sandbox.stub(comp, "render").returns(
);
+
+ comp.componentWillReceiveProps({schema});
+
+ sinon.assert.notCalled(comp.render);
+ });
+ });
});
diff --git a/test/utils_test.js b/test/utils_test.js
index 3a6f9223b4..6c466546dd 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -5,7 +5,8 @@ import {
getDefaultFormState,
isMultiSelect,
mergeObjects,
- retrieveSchema
+ retrieveSchema,
+ shouldRender
} from "../src/utils";
@@ -282,4 +283,59 @@ describe("utils", () => {
expect(retrieveSchema(schema, definitions)).eql(address_definition);
});
});
+
+ describe("shouldRender", () => {
+ const initial = {props: {myProp: 1}, state: {myState: 1}};
+
+ it("should detect equivalent props and state", () => {
+ expect(shouldRender(
+ initial,
+ {myProp: 1},
+ {myState: 1}
+ )).eql(false);
+ });
+
+ it("should detect diffing props", () => {
+ expect(shouldRender(
+ initial,
+ {myProp: 2},
+ {myState: 1}
+ )).eql(true);
+ });
+
+ it("should detect diffing state", () => {
+ expect(shouldRender(
+ initial,
+ {myProp: 1},
+ {myState: 2}
+ )).eql(true);
+ });
+
+ it("should handle equivalent function prop", () => {
+ const fn = () => {};
+ expect(shouldRender(
+ {props: {myProp: fn}, state: {myState: 1}},
+ {myProp: fn},
+ {myState: 1}
+ )).eql(false);
+ });
+
+ it("should handle equivalent function prop with diff identities", () => {
+ expect(shouldRender(
+ {props: {myProp: () => {}}, state: {myState: 1}},
+ {myProp: () => {}},
+ {myState: 1}
+ )).eql(false);
+ });
+
+ it("should handle equivalent bound function prop", () => {
+ const ctx = {};
+ const fn = function(){};
+ expect(shouldRender(
+ {props: {myProp: fn.bind(ctx)}, state: {myState: 1}},
+ {myProp: fn.bind(ctx)},
+ {myState: 1}
+ )).eql(false);
+ });
+ });
});