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); + }); + }); });