codecks-io / react-reform

Helps you create powerful themes for pleasant to use forms
http://react-reform.codecks.io
135 stars 7 forks source link

Debounced Asynchronous validation #23

Closed jagged3dge closed 7 years ago

jagged3dge commented 7 years ago

Is it possible ? Having an input trigger asynchronous (w/ Promises) validation requests at every keystroke is detrimental to the server throughput. I'd like to debounce the asynchronous validation function so that it only triggers after at least 500ms have passed since the last keystroke.

I have tried to figure this out using a few debounced promise implementations published over npm. No luck so far.

Has anyone achieved this?

Here's my relevant asynchronous validation function:

unique: () => {
    const cachedResults = {}

    return {
      isValid: (value, data, revalidate) => {
        if (!value) return true

        if (cachedResults[value] === undefined) {
          cachedResults[value] = 'pending'

          AsyncUniqueValidator(data.arg.toLowerCase())(value)
            .then((isUnique) => {
              cachedResults[value] = isUnique
              revalidate()
            })
        }

        return cachedResults[value]
      },
      errorMessage: (value, { arg }) => `This ${arg.toLowerCase()} is already in use.\nPlease choose a different ${arg.toLowerCase()}`
    }
  },
// ... rest validation rules

AsyncUniqueValidator.js

import fetch from 'isomorphic-fetch'
import apiUrl from 'shared/ApiUrl'
import debounce from 'debounce-promise' // bjoerge/debounce-promise
// import debounce from 'es6-promise-debounce' // digitalbazaar/es6-promise-debounce
// import debounce from 'promise-debounce' // jaz303/promise-debounce

const asyncValidateCheck = (key) => {
  const fn = (value) => {
    return fetch(`${apiUrl}/CheckUnique?${key}=${value.trim()}`, {
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json;charset=UTF-8'
      },
      method: 'post'
    })
      .then((data) => {
        return data.hasOwnProperty('isUnique')
                  ? data.isUnique
                  : false
      })
      .catch((err) => {
        console.log('err =', err)
        return err
      })
  }

  return debounce(fn, 500)
  // return fn
}

export default asyncValidateCheck
// export default debounce(asyncValidateCheck, 500) // doesn't work either

When typing into the textbox, the network tab in Chrome's DevTools looks like this: capture

The image shows no debounce is actually happening. I've tried debouncing the return function altogether. That does no change to the waterfall of network requests on typing.

Would you kindly point me in the right direction?

jagged3dge commented 7 years ago

Seems I jumped the gun a bit. I just had to simplify the asyncValidateCheck function to get it to work. Current setup that works for me:

import fetch from 'isomorphic-fetch'
// import apiUrl from 'shared/ApiUrl'
import debounce from 'debounce-promise' // bjoerge/debounce-promise
// import debounce from 'es6-promise-debounce' // digitalbazaar/es6-promise-debounce
// import debounce from 'promise-debounce' // jaz303/promise-debounce

const asyncValidateCheck = (key, value) => {
  const apiUrl = require('shared/ApiUrl').default

  return fetch(`${apiUrl}/CheckUnique?${key}=${value.trim()}`, {
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json;charset=UTF-8'
    },
    method: 'post'
  })
    .then(checkStatus)
    .then((data) => {
      return data.hasOwnProperty('isUnique')
                ? data.isUnique
                : false
    })
    .catch((err) => {
      log('[AsyncValidator]', 'err =', err)
      return false
    })
}

export default debounce(asyncValidateCheck, 500)

… and in Validations.js

unique: () => {
    const cachedResults = {}

    return {
      isValid: (value, data, revalidate) => {
        if (!value) return true

        const key = data.arg.toLowerCase()
        const cacheKey = key + ':' + value

        if (cachedResults[cacheKey] === undefined) {
          cachedResults[cacheKey] = 'pending'

          AsyncUniqueValidator(key, value)
            .then((isUnique) => {
              cachedResults[cacheKey] = isUnique
              revalidate()
            })
        }

        return cachedResults[cacheKey]
      },
      errorMessage: (value, { arg }) => `This ${arg.toLowerCase()} is already in use.\nPlease choose a different ${arg.toLowerCase()}`
    }
  }
danielberndt commented 7 years ago

yes, looks quite similiar to what I am doing :)

import debounce from "lodash/debounce";

const unique = getUrl => {
  const cachedData = {};

  const check = debounce((value, cb) => {
    cachedData[value] = "pending";
    fetch(getUrl(value)).then(res => {
      if (res.status < 300) {
        cachedData[value] = res.jsonData.ok;
        cb();
      } else {
        return Promise.reject({status: res.status, error: new Error(res.jsonData.error)});
      }
    }).catch(e => console.warn(e));
  }, 200);

  return (value, ctx, validateAgainCb) => {
    if (cachedData[value] === undefined) {
      if (value) {
        check(value, validateAgainCb);
        return "pending";
      } else {
        cachedData[value] = true;
      }
    }
    return cachedData[value];
  };
};

it can be used like this:

  const uniqueEmail = () => {
    return {
      isValid: unique(email => `/services/check-email-unique?${stringify({email})}`),
      errorMessage: () => "This email address is already registered",
      hintMessage: () => "Needs to be a unique email"
    };