Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit f5d05f3

Browse files
authored
Memoize field validation results (#10714)
* Memoize field validation results * Make validation memoization opt-in
1 parent a629ce3 commit f5d05f3

File tree

4 files changed

+85
-55
lines changed

4 files changed

+85
-55
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"matrix-events-sdk": "0.0.1",
9999
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
100100
"matrix-widget-api": "^1.3.1",
101+
"memoize-one": "^5.1.1",
101102
"minimist": "^1.2.5",
102103
"opus-recorder": "^8.0.3",
103104
"pako": "^2.0.3",

src/components/views/auth/PassphraseField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class PassphraseField extends PureComponent<IProps> {
9292
},
9393
},
9494
],
95+
memoize: true,
9596
});
9697

9798
public onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {

src/components/views/directory/NetworkDropdown.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const validServer = withValidation<undefined, { error?: MatrixError }>({
6868
: _t("Can't find this server or its room list"),
6969
},
7070
],
71+
memoize: true,
7172
});
7273

7374
function useSettingsValueWithSetter<T>(

src/components/views/elements/Validation.tsx

Lines changed: 82 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import React, { ReactNode } from "react";
18+
import React, { ReactChild, ReactNode } from "react";
1919
import classNames from "classnames";
20+
import memoizeOne from "memoize-one";
2021

2122
type Data = Pick<IFieldState, "value" | "allowEmpty">;
2223

@@ -40,6 +41,7 @@ interface IArgs<T, D = void> {
4041
description?(this: T, derivedData: D, results: IResult[]): ReactNode;
4142
hideDescriptionIfValid?: boolean;
4243
deriveData?(data: Data): Promise<D>;
44+
memoize?: boolean;
4345
}
4446

4547
export interface IFieldState {
@@ -60,7 +62,7 @@ export interface IValidationResult {
6062
* @param {Function} description
6163
* Function that returns a string summary of the kind of value that will
6264
* meet the validation rules. Shown at the top of the validation feedback.
63-
* @param {Boolean} hideDescriptionIfValid
65+
* @param {boolean} hideDescriptionIfValid
6466
* If true, don't show the description if the validation passes validation.
6567
* @param {Function} deriveData
6668
* Optional function that returns a Promise to an object of generic type D.
@@ -75,6 +77,9 @@ export interface IValidationResult {
7577
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
7678
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
7779
* - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
80+
* @param {boolean?} memoize
81+
* If true, will use memoization to avoid calling deriveData & rules unless the value or allowEmpty change.
82+
* Be careful to not use this if your validation is not pure and depends on other fields, such as "repeat password".
7883
* @returns {Function}
7984
* A validation function that takes in the current input value and returns
8085
* the overall validity and a feedback UI that can be rendered for more detail.
@@ -84,73 +89,87 @@ export default function withValidation<T = void, D = void>({
8489
hideDescriptionIfValid,
8590
deriveData,
8691
rules,
87-
}: IArgs<T, D>) {
88-
return async function onValidate(
92+
memoize,
93+
}: IArgs<T, D>): (fieldState: IFieldState) => Promise<IValidationResult> {
94+
let checkRules = async function (
8995
this: T,
90-
{ value, focused, allowEmpty = true }: IFieldState,
91-
): Promise<IValidationResult> {
92-
if (!value && allowEmpty) {
93-
return {};
94-
}
95-
96-
const data = { value, allowEmpty };
97-
// We know that if deriveData is set then D will not be undefined
98-
const derivedData: D = (await deriveData?.call(this, data)) as D;
99-
96+
data: Data,
97+
derivedData: D,
98+
): Promise<[valid: boolean, results: IResult[]]> {
10099
const results: IResult[] = [];
101100
let valid = true;
102-
if (rules?.length) {
103-
for (const rule of rules) {
104-
if (!rule.key || !rule.test) {
105-
continue;
106-
}
101+
for (const rule of rules) {
102+
if (!rule.key || !rule.test) {
103+
continue;
104+
}
107105

108-
if (!valid && rule.final) {
109-
continue;
110-
}
106+
if (!valid && rule.final) {
107+
continue;
108+
}
109+
110+
if (rule.skip?.call(this, data, derivedData)) {
111+
continue;
112+
}
111113

112-
if (rule.skip?.call(this, data, derivedData)) {
114+
// We're setting `this` to whichever component holds the validation
115+
// function. That allows rules to access the state of the component.
116+
const ruleValid: boolean = await rule.test.call(this, data, derivedData);
117+
valid = valid && ruleValid;
118+
if (ruleValid && rule.valid) {
119+
// If the rule's result is valid and has text to show for
120+
// the valid state, show it.
121+
const text = rule.valid.call(this, derivedData);
122+
if (!text) {
113123
continue;
114124
}
115-
116-
// We're setting `this` to whichever component holds the validation
117-
// function. That allows rules to access the state of the component.
118-
const ruleValid: boolean = await rule.test.call(this, data, derivedData);
119-
valid = valid && ruleValid;
120-
if (ruleValid && rule.valid) {
121-
// If the rule's result is valid and has text to show for
122-
// the valid state, show it.
123-
const text = rule.valid.call(this, derivedData);
124-
if (!text) {
125-
continue;
126-
}
127-
results.push({
128-
key: rule.key,
129-
valid: true,
130-
text,
131-
});
132-
} else if (!ruleValid && rule.invalid) {
133-
// If the rule's result is invalid and has text to show for
134-
// the invalid state, show it.
135-
const text = rule.invalid.call(this, derivedData);
136-
if (!text) {
137-
continue;
138-
}
139-
results.push({
140-
key: rule.key,
141-
valid: false,
142-
text,
143-
});
125+
results.push({
126+
key: rule.key,
127+
valid: true,
128+
text,
129+
});
130+
} else if (!ruleValid && rule.invalid) {
131+
// If the rule's result is invalid and has text to show for
132+
// the invalid state, show it.
133+
const text = rule.invalid.call(this, derivedData);
134+
if (!text) {
135+
continue;
144136
}
137+
results.push({
138+
key: rule.key,
139+
valid: false,
140+
text,
141+
});
145142
}
146143
}
147144

145+
return [valid, results];
146+
};
147+
148+
// We have to memoize it in chunks as `focused` can change frequently, but it isn't passed to these methods
149+
if (memoize) {
150+
if (deriveData) deriveData = memoizeOne(deriveData, isDataEqual);
151+
checkRules = memoizeOne(checkRules, isDerivedDataEqual);
152+
}
153+
154+
return async function onValidate(
155+
this: T,
156+
{ value, focused, allowEmpty = true }: IFieldState,
157+
): Promise<IValidationResult> {
158+
if (!value && allowEmpty) {
159+
return {};
160+
}
161+
162+
const data = { value, allowEmpty };
163+
// We know that if deriveData is set then D will not be undefined
164+
const derivedData = (await deriveData?.call(this, data)) as D;
165+
const [valid, results] = await checkRules.call(this, data, derivedData);
166+
148167
// Hide feedback when not focused
149168
if (!focused) {
150169
return { valid };
151170
}
152171

153-
let details;
172+
let details: ReactNode | undefined;
154173
if (results && results.length) {
155174
details = (
156175
<ul className="mx_Validation_details">
@@ -170,15 +189,15 @@ export default function withValidation<T = void, D = void>({
170189
);
171190
}
172191

173-
let summary;
192+
let summary: ReactNode | undefined;
174193
if (description && (details || !hideDescriptionIfValid)) {
175194
// We're setting `this` to whichever component holds the validation
176195
// function. That allows rules to access the state of the component.
177196
const content = description.call(this, derivedData, results);
178197
summary = content ? <div className="mx_Validation_description">{content}</div> : undefined;
179198
}
180199

181-
let feedback;
200+
let feedback: ReactChild | undefined;
182201
if (summary || details) {
183202
feedback = (
184203
<div className="mx_Validation">
@@ -194,3 +213,11 @@ export default function withValidation<T = void, D = void>({
194213
};
195214
};
196215
}
216+
217+
function isDataEqual([a]: [Data], [b]: [Data]): boolean {
218+
return a.value === b.value && a.allowEmpty === b.allowEmpty;
219+
}
220+
221+
function isDerivedDataEqual([a1, a2]: [Data, any], [b1, b2]: [Data, any]): boolean {
222+
return a2 === b2 && isDataEqual([a1], [b1]);
223+
}

0 commit comments

Comments
 (0)