on master help sign in
fix: stops validation flickering for unecessarilly async validations

see the comment for details, but the gist is that validation was async so final-form was always toggling the `validation` state, which flickers when the only validations we're doing are sync. so now all of the validators conditionally return promises, which helps nicely.

commitdc028a24e5e2e6264b3895822f7cc8837537bc47 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateFri 9 Aug 2019, 18:50 (7 years ago) parent20e3963
5 files changed+105−62
src/form/validators.js+39−15 src/lib/isPromise.js+6 src/views/CreateGalaxy.js+12−9 src/views/IssueChild.js+12−9 src/views/Login/Ticket.js+36−29
src/form/validators.js+39−15
@@ -14,16 +14,18 @@ import {
1414 validateEmail,
1515 validateNotNullAddress,
1616} from 'lib/validators';
17+import isPromise from 'lib/isPromise';
1718
1819// iterate over validators, exiting early if there's an error
19−const buildValidator = (validators = []) => async value => {
20+const buildValidator = (validators = []) => value => {
2021 for (const validator of validators) {
2122 try {
22− const error = await validator(value);
23+ const error = validator(value);
2324 if (error) {
2425 return error;
2526 }
2627 } catch (error) {
28+ console.error(error);
2729 return error.message;
2830 }
2931 }
@@ -93,26 +95,48 @@ export const composeValidator = (
9395 );
9496
9597 // async reduce errors per-field into an errors object
96− const fieldLevelValidator = async values => {
97− const errors = await Promise.all(
98− names.map((name, i) => fieldLevelValidators[i](values[name]))
98+ const fieldLevelValidator = values => {
99+ const errorsOrPromises = names.map((name, i) =>
100+ fieldLevelValidators[i](values[name])
99101 );
100102
101− return names.reduce(
102− (memo, name, i) => ({
103− ...memo,
104− [name]: errors[i],
105− }),
106− {}
107− );
103+ const reduce = errors =>
104+ names.reduce(
105+ (memo, name, i) => ({
106+ ...memo,
107+ [name]: errors[i],
108+ }),
109+ {}
110+ );
111+
112+ if (some(errorsOrPromises, isPromise)) {
113+ // if any of these results are a promise, await them all then reduce
114+ return Promise.all(errorsOrPromises).then(reduce);
115+ }
116+
117+ // otherwise return immediately
118+ return reduce(errorsOrPromises);
108119 };
109120
110121 // the final-form `validate` function
111− return async values => {
122+ // NOTE: if we return a Promise to final-form it will toggle the `validating`
123+ // state, which is expected. If our promise resolves immediately, however,
124+ // that means our `validating` state flickers the UI and it looks pretty bad.
125+ // The solution is to conditionally return a promise only when necessary.
126+ return values => {
112127 // ask for field-level errors
113− const errors = await fieldLevelValidator(values);
128+ const errorsOrPromise = fieldLevelValidator(values);
129+
114130 // pass the current values and their validity to the form-level validator
115131 // that can implement conditional logic and more complex validations
116− return await formValidator(values, errors);
132+ const runFormValidator = errors => formValidator(values, errors);
133+
134+ if (errorsOrPromise.then) {
135+ // if promise, promise
136+ return errorsOrPromise.then(runFormValidator);
137+ }
138+
139+ // otherwise, it's an errors object
140+ return runFormValidator(errorsOrPromise);
117141 };
118142};
src/lib/isPromise.jsnew file+6
@@ -0,0 +1,6 @@
1+// an object is a promise if it has a `.then` function
2+// https://github.com/then/is-promise/blob/master/index.js
3+export default obj =>
4+ !!obj &&
5+ (typeof obj === 'object' || typeof obj === 'function') &&
6+ typeof obj.then === 'function';
src/views/CreateGalaxy.js+12−9
@@ -59,16 +59,16 @@ export default function CreateGalaxy() {
5959 bind,
6060 } = useCreateGalaxy();
6161
62− const validateGalaxy = useCallback(
63− async galaxyName => {
62+ const validateForm = useCallback(
63+ async values => {
6464 const currentOwner = await azimuth.azimuth.getOwner(
6565 _contracts,
66− patp2dec(galaxyName)
66+ patp2dec(values.galaxyName)
6767 );
6868
6969 const isAvailable = isZeroAddress(currentOwner);
7070 if (!isAvailable) {
71− return 'This galaxy is already spawned and owned.';
71+ return { galaxyName: 'This galaxy is already spawned and owned.' };
7272 }
7373 },
7474 [_contracts]
@@ -76,11 +76,14 @@ export default function CreateGalaxy() {
7676
7777 const validate = useMemo(
7878 () =>
79− composeValidator({
80− galaxyName: buildPointValidator(1, [validateGalaxy]),
81− owner: buildAddressValidator(),
82− }),
83− [validateGalaxy]
79+ composeValidator(
80+ {
81+ galaxyName: buildPointValidator(1),
82+ owner: buildAddressValidator(),
83+ },
84+ validateForm
85+ ),
86+ [validateForm]
8487 );
8588
8689 const onValues = useCallback(
src/views/IssueChild.js+12−9
@@ -81,13 +81,13 @@ export default function IssueChild() {
8181 bind,
8282 } = useIssueChild();
8383
84− const validatePoint = useCallback(
85− async name => {
86− const point = patp2dec(name);
84+ const validateForm = useCallback(
85+ async values => {
86+ const point = patp2dec(values.name);
8787 const hasPoint = (await availablePointsPromise).has(point);
8888
8989 if (!hasPoint) {
90− return 'This point cannot be spawned.';
90+ return { point: 'This point cannot be spawned.' };
9191 }
9292 },
9393 [availablePointsPromise]
@@ -95,11 +95,14 @@ export default function IssueChild() {
9595
9696 const validate = useMemo(
9797 () =>
98− composeValidator({
99− point: buildPointValidator(4, [validatePoint]),
100− owner: buildAddressValidator(),
101− }),
102− [validatePoint]
98+ composeValidator(
99+ {
100+ point: buildPointValidator(4),
101+ owner: buildAddressValidator(),
102+ },
103+ validateForm
104+ ),
105+ [validateForm]
103106 );
104107
105108 const onValues = useCallback(
src/views/Login/Ticket.js+36−29
@@ -43,34 +43,13 @@ export default function Ticket({ className, goHome }) {
4343
4444 const cachedUrbitWallet = useRef(Nothing());
4545
46− const validateForm = useCallback(
47− async (values, errors) => {
48− if (errors.point) {
49− return errors;
50− }
51−
52− await timeout(16); // allow ui events to flush
53−
54− let ticket;
55− if (values.useShards) {
56− if (errors.shard1 || errors.shard2 || errors.shard3) {
57− return errors;
58− }
59−
60− ticket = kg.combine([values.shard1, values.shard2, values.shard3]);
61− } else {
62− if (errors.ticket) {
63− return errors;
64− }
65−
66− ticket = values.ticket;
67− }
68−
46+ const validateFormAsync = useCallback(
47+ async (values, ticket) => {
6948 try {
70− // ticket
7149 const _contracts = need.contracts(contracts);
7250 const point = patp2dec(values.point);
7351
52+ await timeout(16); // allow ui events to flush
7453 cachedUrbitWallet.current = await urbitWalletFromTicket(
7554 ticket,
7655 point,
@@ -92,11 +71,12 @@ export default function Ticket({ className, goHome }) {
9271
9372 const noPermissions = !isOwner && !isTransferProxy;
9473 // notify the user, but allow login regardless
95− addWarning({
96− point: noPermissions
97− ? 'This wallet is not the owner or transfer proxy for this point.'
98− : null,
99− });
74+ if (noPermissions) {
75+ addWarning({
76+ point:
77+ 'This wallet is not the owner or transfer proxy for this point.',
78+ });
79+ }
10080 } catch (error) {
10181 console.error(error);
10282 return {
@@ -109,6 +89,33 @@ export default function Ticket({ className, goHome }) {
10989 [addWarning, contracts]
11090 );
11191
92+ const validateForm = useCallback(
93+ (values, errors) => {
94+ if (errors.point) {
95+ addWarning({ point: null });
96+ return errors;
97+ }
98+
99+ let ticket;
100+ if (values.useShards) {
101+ if (errors.shard1 || errors.shard2 || errors.shard3) {
102+ return errors;
103+ }
104+
105+ ticket = kg.combine([values.shard1, values.shard2, values.shard3]);
106+ } else {
107+ if (errors.ticket) {
108+ return errors;
109+ }
110+
111+ ticket = values.ticket;
112+ }
113+
114+ return validateFormAsync(values, ticket);
115+ },
116+ [addWarning, validateFormAsync]
117+ );
118+
112119 const validate = useMemo(
113120 () =>
114121 composeValidator(