Skip to content

Commit 2cefad8

Browse files
committed
performance optimization and support improvements
- refactor form attributes update routines to get rid of `cloneDeep` calls: deep cloning is unefficient since it potentially most of cloned data should not be cloned. instead, implement `update` and `updated` helper functions that carefully clone only path-related collections. - refactor code to depend only on 'lodash.get' and 'lodash.set' packages - extend form from `Component` if `PureComponent` is unavailable. this allows to support older versions of react - corresponding package.json updates - minor update README update
1 parent 9a26b4f commit 2cefad8

File tree

6 files changed

+86
-39
lines changed

6 files changed

+86
-39
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@
3131
"react/jsx-equals-spacing": [1, "never"],
3232
"react/jsx-indent-props": [1, 2],
3333
"react/jsx-indent": [1, 2],
34-
"react/jsx-no-duplicate-props": 2,
34+
"react/jsx-no-duplicate-props": 2
3535
}
3636
}

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ Details on how to run it locally are at the end of README.
4141
Most of forms developers deal with are quite complicated and encapsulate
4242
vast amount of validation and rendering logic. After some basic setup described
4343
in the [Wiki](https://github.com/akuzko/react-form-base/wiki) your form may
44-
look like following:
44+
look like following (please note that `$render` is a helper method and it is
45+
possible to use classic `render` method with a slightly more verbose result):
4546

4647
```js
4748
class UserForm extends Form {

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@
5050
"rimraf": "^2.5.4"
5151
},
5252
"dependencies": {
53-
"lodash": "^4.16.4"
53+
"lodash.get": "^4.4.2",
54+
"lodash.set": "^4.3.2"
5455
},
5556
"peerDependencies": {
56-
"react": ">= 15.0.0",
57-
"react-dom": ">= 15.0.0"
57+
"react": "^0.14.8 || >=15.0.0",
58+
"react-dom": "^0.14.8 || >=15.0.0"
5859
}
5960
}

src/Form.jsx

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

3-
import { nameToPath, buildFormValidator } from './utils';
4-
import isPlainObject from 'lodash/isPlainObject';
5-
import cloneDeep from 'lodash/cloneDeep';
6-
import get from 'lodash/get';
7-
import set from 'lodash/set';
8-
import noop from 'lodash/noop';
3+
import { update, buildFormValidator, noop } from './utils';
4+
import get from 'lodash.get';
95

10-
export default class Form extends PureComponent {
6+
export default class Form extends (PureComponent || Component) {
117
static propTypes = {
128
attrs: PropTypes.object.isRequired,
139
onChange: PropTypes.func,
@@ -76,27 +72,27 @@ export default class Form extends PureComponent {
7672
get(name) {
7773
if (name === undefined) return this.props.attrs;
7874

79-
return get(this.props.attrs, nameToPath(name));
75+
return get(this.props.attrs, name.split('.'));
8076
}
8177

8278
set(name, value) {
83-
if (isPlainObject(name)) return this._setObject(name);
79+
if (name && (typeof name === 'object') && (name.constructor === Object)) return this._setObject(name);
8480

8581
return this._setAttr(name, value);
8682
}
8783

8884
_setObject(obj) {
8985
return this._set((attrs, errors) => {
9086
for (const name in obj) {
91-
set(attrs, nameToPath(name), obj[name]);
87+
update(attrs, name, obj[name], false);
9288
this._updateErrors(errors, name, obj[name]);
9389
}
9490
});
9591
}
9692

9793
_setAttr(name, value) {
9894
return this._set((attrs, errors) => {
99-
set(attrs, nameToPath(name), value);
95+
update(attrs, name, value, false);
10096
this._updateErrors(errors, name, value);
10197
});
10298
}
@@ -113,13 +109,12 @@ export default class Form extends PureComponent {
113109

114110
_set(updater) {
115111
const { attrs, onChange } = this.props;
116-
const newAttrs = cloneDeep(attrs);
117-
const newErrors = { ...this.getErrors() };
112+
const nextAttrs = { ...attrs };
113+
const nextErrors = { ...this.getErrors() };
114+
updater(nextAttrs, nextErrors);
118115

119-
updater(newAttrs, newErrors);
120-
121-
this._nextErrors = newErrors;
122-
onChange(newAttrs);
116+
this._nextErrors = nextErrors;
117+
onChange(nextAttrs);
123118
}
124119

125120
_shouldClearError(name) {
@@ -165,10 +160,8 @@ export default class Form extends PureComponent {
165160

166161
merge(name, value) {
167162
const current = this.get(name) || {};
168-
const attrs = cloneDeep(this.props.attrs);
169163

170-
set(attrs, name, { ...current, ...value });
171-
this.props.onChange(attrs);
164+
return this.set(name, { ...current, ...value });
172165
}
173166

174167
push(name, value) {
@@ -181,7 +174,7 @@ export default class Form extends PureComponent {
181174
const ary = this.get(name);
182175

183176
return this._set((attrs, errors) => {
184-
set(attrs, nameToPath(name), [...ary.slice(0, i), ...ary.slice(i + 1)]);
177+
update(attrs, name, [...ary.slice(0, i), ...ary.slice(i + 1)]);
185178
this._updateErrors(errors, `${name}.${i}`, null);
186179
});
187180
}

src/utils.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import isArray from 'lodash/isArray';
2-
import isPlainObject from 'lodash/isPlainObject';
1+
import set from 'lodash.set';
2+
3+
export function noop(){}
34

45
export function bindState(component, key = 'form') {
56
return {
@@ -8,8 +9,31 @@ export function bindState(component, key = 'form') {
89
};
910
}
1011

11-
export function nameToPath(name) {
12-
return name.replace(/\.(\d+)(\.)?/g, (_match, i, dot) => `[${i}]` + (dot ? '.' : ''));
12+
export function update(obj, name, value) {
13+
_update(obj, name, value);
14+
15+
return obj;
16+
}
17+
18+
export function updated(obj, name, value) {
19+
const current = { ...obj };
20+
21+
return update(current, name, value);
22+
}
23+
24+
function _update(current, name, value) {
25+
const match = name.match(/^([\w\d]+)\.?(.+)?$/);
26+
const { 1: key, 2: rest } = match;
27+
28+
if (current[key] === undefined) {
29+
return set(current, name.split('.'), value);
30+
}
31+
if (!rest) {
32+
return current[key] = value;
33+
}
34+
35+
current[key] = Array.isArray(current[key]) ? [...current[key]] : { ...current[key] };
36+
_update(current[key], rest, value);
1337
}
1438

1539
function wildcard(name) {
@@ -32,10 +56,7 @@ export function buildFormValidator(form) {
3256
return null;
3357

3458
function callValidator(validator) {
35-
if (isPlainObject(validator)) {
36-
return callObjectValidator(validator);
37-
}
38-
if (isArray(validator)) {
59+
if (Array.isArray(validator)) {
3960
return callArrayValidator(validator);
4061
}
4162
if (typeof validator === 'string') {
@@ -44,6 +65,9 @@ export function buildFormValidator(form) {
4465
if (typeof validator === 'function') {
4566
return validator.call(form, value);
4667
}
68+
if (validator && (typeof validator === 'object')) {
69+
return callObjectValidator(validator);
70+
}
4771
throw new Error(`unable to use '${validator}' as validator function`);
4872
}
4973

test/utils.test.js

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

7-
describe('nameToPath', function() {
8-
it('generates a lodash path based on passed name', function() {
9-
expect(nameToPath('foo.1.bar.baz')).toEqual('foo[1].bar.baz');
7+
describe('updated', function() {
8+
it('carefully sets deeply nested item: deeply nested array', function() {
9+
const obj = { foo: { bar: { baz: [1, 2, 3] } }, bak: { big: 1 } };
10+
const upd = updated(obj, 'foo.bar.baz.1', 4);
11+
12+
expect(obj === upd).toBe(false, 'obj should not be updated in place');
13+
expect(obj.foo === upd.foo).toBe(false, 'obj.foo should not be updated in place');
14+
expect(obj.foo.bar === upd.foo.bar).toBe(false, 'obj.foo.bar should not be updated in place');
15+
expect(obj.foo.bar.baz === upd.foo.bar.baz).toBe(false, 'obj.foo.bar.baz should not be updated in place');
16+
expect(obj.bak === upd.bak).toBe(true, 'obj.bak should not be cloned');
17+
expect(upd.foo.bar.baz).toMatch([1, 4, 3], 'value under desired name should be updated');
18+
});
19+
20+
it('carefully sets deeply nested item: deeply nested object', function() {
21+
const obj = { foo: { bar: [{ baz: 'baz1' }, { baz: 'baz2' }] }, bak: { big: 1 } };
22+
const upd = updated(obj, 'foo.bar.1.baz', 'baz3');
23+
24+
expect(obj === upd).toBe(false, 'obj should not be updated in place');
25+
expect(obj.foo === upd.foo).toBe(false, 'obj.foo should not be updated in place');
26+
expect(obj.foo.bar === upd.foo.bar).toBe(false, 'obj.foo.bar should not be updated in place');
27+
expect(obj.foo.bar[0] === upd.foo.bar[0]).toBe(true, 'obj.foo.bar items should not be cloned');
28+
expect(obj.bak === upd.bak).toBe(true, 'obj.bak should not be cloned');
29+
expect(upd.foo.bar[1]).toMatch({ baz: 'baz3' }, 'value under desired name should be updated');
30+
});
31+
32+
it('carefully sets deeply nested item, path collections are not defined', function() {
33+
const obj = { bak: { big: 1 } };
34+
const upd = updated(obj, 'foo.bar.baz.1', 4);
35+
36+
expect(obj.bak === upd.bak).toBe(true, 'obj.bak should not be cloned');
37+
expect(upd.foo.bar.baz).toMatch([undefined, 4], 'value under desired name should be updated');
1038
});
1139
});
1240

0 commit comments

Comments
 (0)