Skip to content

Commit 7d60f8d

Browse files
committed
feat(manager): prepare API for rendering based on state changes
1 parent 67f2338 commit 7d60f8d

File tree

4 files changed

+120
-11
lines changed

4 files changed

+120
-11
lines changed

packages/form-state-manager/src/tests/performance/field-render-cycle.test.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const Field = ({ fieldSpy, ...props }) => {
1313
return <input id={id} {...input} {...rest} />;
1414
};
1515

16-
const TestSubject = ({ fieldSpy }) => (
17-
<FormStateManager onSubmit={jest.fn()}>
16+
const TestSubject = ({ fieldSpy, subscription }) => (
17+
<FormStateManager onSubmit={jest.fn()} subscription={subscription}>
1818
{() => {
1919
return (
2020
<form onSubmit={jest.fn()}>
@@ -30,7 +30,7 @@ const TestSubject = ({ fieldSpy }) => (
3030
describe('useField rendering cycle', () => {
3131
it('should render first field twice and second once', () => {
3232
const fieldSpy = jest.fn();
33-
const wrapper = mount(<TestSubject fieldSpy={fieldSpy} />);
33+
const wrapper = mount(<TestSubject fieldSpy={fieldSpy} subscription={{}} />);
3434
/**
3535
* Initial mount render of both fields
3636
*/
@@ -52,4 +52,22 @@ describe('useField rendering cycle', () => {
5252
expect(fieldSpy.mock.calls[0][0]).toEqual('one');
5353
expect(fieldSpy.mock.calls[1][0]).toEqual('one');
5454
});
55+
56+
it('should render both fields when subscription {values: true} on change', () => {
57+
const fieldSpy = jest.fn();
58+
const wrapper = mount(<TestSubject fieldSpy={fieldSpy} subscription={{ values: true }} />);
59+
60+
expect(fieldSpy).toHaveBeenCalledTimes(2);
61+
expect(fieldSpy.mock.calls[0][0]).toEqual('one');
62+
expect(fieldSpy.mock.calls[1][0]).toEqual('two');
63+
fieldSpy.mockReset();
64+
65+
act(() => {
66+
wrapper.find('input#one').prop('onChange')({ target: { value: 'foo', type: 'text' } });
67+
});
68+
69+
expect(fieldSpy).toHaveBeenCalledTimes(2);
70+
expect(fieldSpy.mock.calls[0][0]).toEqual('one');
71+
expect(fieldSpy.mock.calls[1][0]).toEqual('two');
72+
});
5573
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import findDifference from '../../utils/find-difference';
2+
3+
describe('findDifference', () => {
4+
let oldState;
5+
let newState;
6+
7+
it('should skip functions', () => {
8+
oldState = {
9+
pristine: false,
10+
function: () => 'bbb'
11+
};
12+
13+
newState = {
14+
pristine: true,
15+
function: () => 'aaa'
16+
};
17+
18+
expect(findDifference(oldState, newState)).toEqual(['pristine']);
19+
});
20+
21+
it('should skip keys from denyList', () => {
22+
oldState = {
23+
fieldListeners: { name: 'pepa' }
24+
};
25+
26+
newState = {
27+
fieldListeners: { name: 'john' }
28+
};
29+
30+
expect(findDifference(oldState, newState)).toEqual([]);
31+
});
32+
33+
it('should find different keys', () => {
34+
oldState = {
35+
pristine: true,
36+
dirty: false,
37+
values: {
38+
nested: 'a'
39+
},
40+
initialValues: {
41+
nested: 'b'
42+
}
43+
};
44+
45+
newState = {
46+
pristine: false,
47+
dirty: false,
48+
values: {
49+
nested: 'aa'
50+
},
51+
initialValues: {
52+
nested: 'b'
53+
}
54+
};
55+
56+
expect(findDifference(oldState, newState)).toEqual(['pristine', 'values']);
57+
});
58+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ManagerState } from '../types/manager-api';
2+
import isEqual from 'lodash/isEqual';
3+
4+
const denyList = ['fieldListeners'];
5+
6+
function findDifference(oldState: ManagerState, newState: ManagerState): Array<keyof ManagerState> {
7+
const changed: Array<keyof ManagerState> = [];
8+
9+
const keys = Object.keys(oldState)
10+
.map((key) => (typeof oldState[key as keyof ManagerState] !== 'function' && !denyList.includes(key) ? key : undefined))
11+
.filter(Boolean);
12+
13+
keys.forEach((key) => {
14+
if (key && !isEqual(oldState[key as keyof ManagerState], newState[key as keyof ManagerState])) {
15+
changed.push(key as keyof ManagerState);
16+
}
17+
});
18+
19+
return changed;
20+
}
21+
22+
export default findDifference;

packages/form-state-manager/src/utils/manager-api.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import FieldConfig, { IsEqual } from '../types/field-config';
2626
import { Meta } from '../types/use-field';
2727
import { formLevelValidator, isPromise } from './validate';
2828
import { FormValidator, FormLevelError, Validator } from '../types/validate';
29+
import findDifference from './find-difference';
2930

3031
export const defaultIsEqual = (a: any, b: any) => a === b;
3132

@@ -418,17 +419,24 @@ const createManagerApi: CreateManagerApi = ({
418419
});
419420
}
420421

421-
function change(name: string, value?: any): void {
422-
set(state.values, name, value);
423-
state.visited[name] = true;
424-
state.modified[name] = true;
425-
state.modifiedSinceLastSubmit = true;
426-
state.dirtySinceLastSubmit = true;
427-
state.dirtyFields[name] = true;
428-
state.dirtyFieldsSinceLastSubmit[name] = true;
422+
function prepareRerender() {
423+
const snapshot = cloneDeep(state);
424+
425+
return (subscribeTo: Array<string> = []) => rerender([...findDifference(snapshot, state), ...subscribeTo]);
426+
}
429427

428+
function change(name: string, value?: any): void {
430429
// TODO modify all affected field state variables
431430
batch(() => {
431+
const render = prepareRerender();
432+
set(state.values, name, value);
433+
state.visited[name] = true;
434+
state.modified[name] = true;
435+
state.modifiedSinceLastSubmit = true;
436+
state.dirtySinceLastSubmit = true;
437+
state.dirtyFields[name] = true;
438+
state.dirtyFieldsSinceLastSubmit[name] = true;
439+
432440
const allIsEqual: Array<IsEqual> = state.fieldListeners[name]
433441
? Object.values(state.fieldListeners[name].fields)
434442
.map(({ isEqual }) => isEqual as IsEqual, [])
@@ -457,11 +465,14 @@ const createManagerApi: CreateManagerApi = ({
457465
if (config.validate) {
458466
validateForm(config.validate);
459467
}
468+
469+
render();
460470
});
461471
}
462472

463473
function focus(name: string): void {
464474
state.active = name;
475+
state.visited[name] = true;
465476
setFieldState(name, (prevState) => ({ ...prevState, meta: { ...prevState.meta, active: true } }));
466477
}
467478

0 commit comments

Comments
 (0)