Skip to content

Commit 05aa676

Browse files
committed
add pure render optimization
- cache generated input change handlers in order to prevent re-rendering of pure render input components
1 parent 0ac83a3 commit 05aa676

File tree

3 files changed

+100
-8
lines changed

3 files changed

+100
-8
lines changed

src/Form.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PropTypes, PureComponent, Component } from 'react';
22

3-
import { update, buildFormValidator, noop } from './utils';
3+
import { update, noop, buildFormValidator, buildHandlersCache } from './utils';
44
import get from 'lodash.get';
55

66
export default class Form extends (PureComponent || Component) {
@@ -26,6 +26,7 @@ export default class Form extends (PureComponent || Component) {
2626
state = { errors: {} };
2727
validations = {};
2828
validator = buildFormValidator(this);
29+
cache = buildHandlersCache();
2930

3031
componentWillReceiveProps() {
3132
if (this._nextErrors) {
@@ -46,14 +47,19 @@ export default class Form extends (PureComponent || Component) {
4647
}
4748

4849
$(name) {
50+
const handler = this.cache.fetch(name, () => this.set.bind(this, name));
51+
4952
const wrapper = (handler, ...bindings) => {
50-
wrapper.onChange = handler.hasOwnProperty('prototype') ? handler.bind(this, ...bindings) : handler;
53+
wrapper.onChange = this.cache.fetch([name, handler, ...bindings], () => {
54+
return handler.hasOwnProperty('prototype') ? handler.bind(this, ...bindings) : handler;
55+
});
56+
5157
return wrapper;
5258
};
5359
Object.defineProperty(wrapper, 'name', { value: name, enumerable: true });
5460
Object.assign(wrapper, {
5561
value: this.get(name),
56-
onChange: this.set.bind(this, name),
62+
onChange: handler,
5763
error: this.getError(name)
5864
});
5965

src/utils.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* global WeakMap */
12
import set from 'lodash.set';
23

34
export function noop(){}
@@ -11,7 +12,7 @@ export function bindState(component, key = 'form') {
1112

1213
export function update(obj, name, value) {
1314
_update(obj, name, value);
14-
15+
1516
return obj;
1617
}
1718

@@ -118,3 +119,66 @@ export function buildFormValidator(form) {
118119

119120
return validate;
120121
}
122+
123+
export function buildHandlersCache() {
124+
return new Cache;
125+
}
126+
127+
class Cache {
128+
constructor() {
129+
this.store = {};
130+
}
131+
132+
fetch(key, setter) {
133+
try {
134+
if (Array.isArray(key)) {
135+
return this.fetchComplex(key, setter);
136+
} else {
137+
return this.fetchSimple(key, setter);
138+
}
139+
} catch (_e) {
140+
return setter();
141+
}
142+
}
143+
144+
fetchSimple(key, setter) {
145+
if (this.store[key]) {
146+
return this.store[key];
147+
} else {
148+
this.store[key] = setter();
149+
return this.store[key];
150+
}
151+
}
152+
153+
fetchComplex([name, ...path], setter) {
154+
name = `_${name}`;
155+
let current = this.store[name] || (this.store[name] = new WeakMap);
156+
157+
for (let i = 0; i < path.length - 1; i++) {
158+
if (typeof this.get(current, path[i]) == 'undefined') {
159+
const nextKey = path[i + 1];
160+
this.put(current, path[i], typeof nextKey === 'number' || typeof nextKey === 'string' || typeof nextKey === 'boolean' ? {} : new WeakMap);
161+
}
162+
current = this.get(current, path[i]);
163+
}
164+
const key = path[path.length - 1], cached = this.get(current, key);
165+
return cached || this.put(current, key, setter());
166+
}
167+
168+
get(store, key) {
169+
if (store.constructor === WeakMap) {
170+
return store.get(key);
171+
} else {
172+
return store[key];
173+
}
174+
}
175+
176+
put(store, key, value) {
177+
if (store.constructor === WeakMap) {
178+
store.set(key, value);
179+
} else {
180+
store[key] = value;
181+
}
182+
return value;
183+
}
184+
}

test/utils.test.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import React, { Component } from 'react';
22
import Form from '../src/Form';
3-
import { bindState, updated } from '../src/utils';
3+
import { bindState, updated, buildHandlersCache } from '../src/utils';
44
import { shallow } from 'enzyme';
55
import expect from 'expect';
66

77
describe('updated', function() {
88
it('carefully sets deeply nested item: deeply nested array', function() {
99
const obj = { foo: { bar: { baz: [1, 2, 3] } }, bak: { big: 1 } };
1010
const upd = updated(obj, 'foo.bar.baz.1', 4);
11-
11+
1212
expect(obj === upd).toBe(false, 'obj should not be updated in place');
1313
expect(obj.foo === upd.foo).toBe(false, 'obj.foo should not be updated in place');
1414
expect(obj.foo.bar === upd.foo.bar).toBe(false, 'obj.foo.bar should not be updated in place');
@@ -20,7 +20,7 @@ describe('updated', function() {
2020
it('carefully sets deeply nested item: deeply nested object', function() {
2121
const obj = { foo: { bar: [{ baz: 'baz1' }, { baz: 'baz2' }] }, bak: { big: 1 } };
2222
const upd = updated(obj, 'foo.bar.1.baz', 'baz3');
23-
23+
2424
expect(obj === upd).toBe(false, 'obj should not be updated in place');
2525
expect(obj.foo === upd.foo).toBe(false, 'obj.foo should not be updated in place');
2626
expect(obj.foo.bar === upd.foo.bar).toBe(false, 'obj.foo.bar should not be updated in place');
@@ -32,7 +32,7 @@ describe('updated', function() {
3232
it('carefully sets deeply nested item, path collections are not defined', function() {
3333
const obj = { bak: { big: 1 } };
3434
const upd = updated(obj, 'foo.bar.baz.1', 4);
35-
35+
3636
expect(obj.bak === upd.bak).toBe(true, 'obj.bak should not be cloned');
3737
expect(upd.foo.bar.baz).toMatch([undefined, 4], 'value under desired name should be updated');
3838
});
@@ -80,3 +80,25 @@ describe('bindState', function() {
8080
expect(wrapper.state()).toEqual({ form: { foo: 'bar' } });
8181
});
8282
});
83+
84+
describe('buildHandlersCache', function() {
85+
context('simple case', function() {
86+
it('fetches result and caches it', function() {
87+
const cache = buildHandlersCache();
88+
const result = cache.fetch('foo', () => ({}));
89+
expect(cache.fetch('foo', () => 'is not called') === result).toBe(true);
90+
});
91+
});
92+
93+
context('complex case', function() {
94+
it('fetches result and caches it', function() {
95+
const cache = buildHandlersCache();
96+
const fn = function(){};
97+
const obj = {};
98+
const result = cache.fetch(['foo', fn, 1, obj, 5], () => fn);
99+
100+
expect(result).toBe(fn, 'sets result according to setter');
101+
expect(cache.fetch(['foo', fn, 1, obj, 5], () => 'is not called') === result).toBe(true, 'returns cached value properly');
102+
});
103+
});
104+
});

0 commit comments

Comments
 (0)