From d10f71b7cfaedba9f457e2b90f28df0707a8edd6 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 1 Jul 2020 15:25:17 -0400 Subject: [PATCH] improves Form performance by storing context value in state --- src/auth/operations/policies.js | 6 +- .../Form/{reducer.js => fieldReducer.js} | 0 src/client/components/forms/Form/index.js | 438 +++++++++--------- .../components/forms/Form/initContextState.js | 16 + .../forms/field-types/Email/index.js | 14 +- .../components/forms/useFieldType/index.js | 1 + .../components/forms/withCondition/index.js | 40 +- 7 files changed, 286 insertions(+), 229 deletions(-) rename src/client/components/forms/Form/{reducer.js => fieldReducer.js} (100%) create mode 100644 src/client/components/forms/Form/initContextState.js diff --git a/src/auth/operations/policies.js b/src/auth/operations/policies.js index 6ee3e9b553..cdfff6c505 100644 --- a/src/auth/operations/policies.js +++ b/src/auth/operations/policies.js @@ -11,7 +11,7 @@ const policies = async (args) => { const promises = []; const isLoggedIn = !!(user); - const collectionConfig = (user && user.collection) ? config.collections.find(collection => collection.slug === user.collection) : null; + const userCollectionConfig = (user && user.collection) ? config.collections.find(collection => collection.slug === user.collection) : null; const createPolicyPromise = async (obj, policy, operation) => { const updatedObj = obj; @@ -71,8 +71,8 @@ const policies = async (args) => { }; try { - if (collectionConfig) { - results.canAccessAdmin = collectionConfig.policies.admin ? collectionConfig.policies.admin(args) : isLoggedIn; + if (userCollectionConfig) { + results.canAccessAdmin = userCollectionConfig.policies.admin ? userCollectionConfig.policies.admin(args) : isLoggedIn; } else { results.canAccessAdmin = false; } diff --git a/src/client/components/forms/Form/reducer.js b/src/client/components/forms/Form/fieldReducer.js similarity index 100% rename from src/client/components/forms/Form/reducer.js rename to src/client/components/forms/Form/fieldReducer.js diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 77cabd111a..7781719ff5 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -1,5 +1,5 @@ import React, { - useState, useReducer, useCallback, useEffect, + useReducer, useEffect, useState, useRef, } from 'react'; import { objectToFormData } from 'object-to-formdata'; import { useHistory } from 'react-router-dom'; @@ -12,7 +12,8 @@ import { useStatusList } from '../../elements/Status'; import { requests } from '../../../api'; import useThrottledEffect from '../../../hooks/useThrottledEffect'; import { useUser } from '../../data/User'; -import fieldReducer from './reducer'; +import fieldReducer from './fieldReducer'; +import initContextState from './initContextState'; import './index.scss'; @@ -49,208 +50,149 @@ const Form = (props) => { } = props; const [fields, dispatchFields] = useReducer(fieldReducer, {}); - const [submitted, setSubmitted] = useState(false); - const [modified, setModified] = useState(false); - const [processing, setProcessing] = useState(false); + const [contextState, setContextState] = useState(initContextState({ dispatchFields })); + const fieldsRef = useRef(fields); + fieldsRef.current = fields; + + const { + createFormData, validateForm, setSubmitted, setModified, setProcessing, submit, + } = contextState; + const history = useHistory(); const locale = useLocale(); const { replaceStatus, addStatus, clearStatus } = useStatusList(); const { refreshToken } = useUser(); - const getFields = useCallback(() => { - return fields; - }, [fields]); + useEffect(() => { + const handleSubmit = (e) => { + e.stopPropagation(); + setSubmitted(true); - const getField = useCallback((path) => { - return fields[path]; - }, [fields]); + const isValid = validateForm(); - const getData = useCallback(() => { - return reduceFieldsToValues(fields, true); - }, [fields]); + // If not valid, prevent submission + if (!isValid) { + e.preventDefault(); - const getSiblingData = useCallback((path) => { - let siblingFields = fields; - - // If this field is nested - // We can provide a list of sibling fields - if (path.indexOf('.') > 0) { - const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1); - siblingFields = Object.keys(fields).reduce((siblings, fieldKey) => { - if (fieldKey.indexOf(parentFieldPath) === 0) { - return { - ...siblings, - [fieldKey.replace(parentFieldPath, '')]: fields[fieldKey], - }; - } - - return siblings; - }, {}); - } - - return reduceFieldsToValues(siblingFields, true); - }, [fields]); - - const getDataByPath = useCallback((path) => { - const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1); - const name = path.split('.').pop(); - - const data = Object.keys(fields).reduce((matchedData, key) => { - if (key.indexOf(`${path}.`) === 0) { - return { - ...matchedData, - [key.replace(pathPrefixToRemove, '')]: fields[key], - }; - } - - return matchedData; - }, {}); - - const values = reduceFieldsToValues(data, true); - const unflattenedData = unflatten(values); - return unflattenedData?.[name]; - }, [fields]); - - const getUnflattenedValues = useCallback(() => { - return reduceFieldsToValues(fields); - }, [fields]); - - const validateForm = useCallback(() => { - return !Object.values(fields).some((field) => { - return field.valid === false; - }); - }, [fields]); - - const createFormData = useCallback(() => { - const data = reduceFieldsToValues(fields); - return objectToFormData(data, { indices: true }); - }, [fields]); - - const submit = useCallback((e) => { - e.stopPropagation(); - setSubmitted(true); - - const isValid = validateForm(); - - // If not valid, prevent submission - if (!isValid) { - e.preventDefault(); - - addStatus({ - message: 'Please correct the fields below.', - type: 'error', - }); - - window.scrollTo({ - top: 0, - behavior: 'smooth', - }); - - return false; - } - - // If submit handler comes through via props, run that - if (onSubmit) { - e.preventDefault(); - return onSubmit(fields); - } - - // If form is AJAX, fetch data - if (ajax !== false) { - e.preventDefault(); - - window.scrollTo({ - top: 0, - behavior: 'smooth', - }); - - const formData = createFormData(); - - setProcessing(true); - // Make the API call from the action - return requests[method.toLowerCase()](action, { - body: formData, - }).then((res) => { - setModified(false); - if (typeof handleAjaxResponse === 'function') return handleAjaxResponse(res); - - return res.json().then((json) => { - setProcessing(false); - clearStatus(); - - if (res.status < 400) { - if (typeof onSuccess === 'function') onSuccess(json); - - if (redirect) { - return history.push(redirect, json); - } - - if (!disableSuccessStatus) { - replaceStatus([{ - message: json.message, - type: 'success', - disappear: 3000, - }]); - } - } else { - if (json.message) { - addStatus({ - message: json.message, - type: 'error', - }); - - return json; - } - - if (Array.isArray(json.errors)) { - const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => { - return err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]; - }, [[], []]); - - fieldErrors.forEach((err) => { - dispatchFields({ - valid: false, - errorMessage: err.message, - path: err.field, - value: fields?.[err.field]?.value, - }); - }); - - nonFieldErrors.forEach((err) => { - addStatus({ - message: err.message || 'An unknown error occurred.', - type: 'error', - }); - }); - - if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { - addStatus({ - message: 'Please correct the fields below.', - type: 'error', - }); - } - - return json; - } - - addStatus({ - message: 'An unknown error occurred.', - type: 'error', - }); - } - - return json; - }); - }).catch((err) => { addStatus({ - message: err, + message: 'Please correct the fields below.', type: 'error', }); - }); - } - return true; + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + + return false; + } + + // If submit handler comes through via props, run that + if (onSubmit) { + e.preventDefault(); + return onSubmit(fields); + } + + // If form is AJAX, fetch data + if (ajax !== false) { + e.preventDefault(); + + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + + const formData = createFormData(); + setProcessing(true); + + // Make the API call from the action + return requests[method.toLowerCase()](action, { + body: formData, + }).then((res) => { + setModified(false); + if (typeof handleAjaxResponse === 'function') return handleAjaxResponse(res); + + return res.json().then((json) => { + setProcessing(false); + clearStatus(); + + if (res.status < 400) { + if (typeof onSuccess === 'function') onSuccess(json); + + if (redirect) { + return history.push(redirect, json); + } + + if (!disableSuccessStatus) { + replaceStatus([{ + message: json.message, + type: 'success', + disappear: 3000, + }]); + } + } else { + if (json.message) { + addStatus({ + message: json.message, + type: 'error', + }); + + return json; + } + + if (Array.isArray(json.errors)) { + const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => { + return err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]; + }, [[], []]); + + fieldErrors.forEach((err) => { + dispatchFields({ + valid: false, + errorMessage: err.message, + path: err.field, + value: fields?.[err.field]?.value, + }); + }); + + nonFieldErrors.forEach((err) => { + addStatus({ + message: err.message || 'An unknown error occurred.', + type: 'error', + }); + }); + + if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { + addStatus({ + message: 'Please correct the fields below.', + type: 'error', + }); + } + + return json; + } + + addStatus({ + message: 'An unknown error occurred.', + type: 'error', + }); + } + + return json; + }); + }).catch((err) => { + addStatus({ + message: err, + type: 'error', + }); + }); + } + + return true; + }; + contextState.submit = handleSubmit; + setContextState(contextState); }, [ + contextState, action, addStatus, ajax, @@ -266,23 +208,115 @@ const Form = (props) => { onSuccess, replaceStatus, createFormData, + setProcessing, + setModified, + setSubmitted, ]); + useEffect(() => { + contextState.getFields = () => { + return fieldsRef.current; + }; + + contextState.getField = (path) => { + return fieldsRef.current[path]; + }; + + contextState.getData = () => { + return reduceFieldsToValues(fieldsRef.current, true); + }; + + contextState.getSiblingData = (path) => { + let siblingFields = fieldsRef.current; + + // If this field is nested + // We can provide a list of sibling fields + if (path.indexOf('.') > 0) { + const parentFieldPath = path.substring(0, path.lastIndexOf('.') + 1); + siblingFields = Object.keys(fieldsRef.current).reduce((siblings, fieldKey) => { + if (fieldKey.indexOf(parentFieldPath) === 0) { + return { + ...siblings, + [fieldKey.replace(parentFieldPath, '')]: fieldsRef.current[fieldKey], + }; + } + + return siblings; + }, {}); + } + + return reduceFieldsToValues(siblingFields, true); + }; + + contextState.getDataByPath = (path) => { + const pathPrefixToRemove = path.substring(0, path.lastIndexOf('.') + 1); + const name = path.split('.').pop(); + + const data = Object.keys(fieldsRef.current).reduce((matchedData, key) => { + if (key.indexOf(`${path}.`) === 0) { + return { + ...matchedData, + [key.replace(pathPrefixToRemove, '')]: fieldsRef.current[key], + }; + } + + return matchedData; + }, {}); + + const values = reduceFieldsToValues(data, true); + const unflattenedData = unflatten(values); + return unflattenedData?.[name]; + }; + + contextState.getUnflattenedValues = () => { + return reduceFieldsToValues(fieldsRef.current); + }; + + contextState.validateForm = () => { + return !Object.values(fieldsRef.current).some((field) => { + return field.valid === false; + }); + }; + + contextState.createFormData = () => { + const data = reduceFieldsToValues(fieldsRef.current); + return objectToFormData(data, { indices: true }); + }; + + setContextState(contextState); + }, [fieldsRef, contextState]); + + useEffect(() => { + function setModifiedState(modified) { + contextState.modified = modified; + } + + function setSubmittedState(submitted) { + contextState.submitted = submitted; + } + + function setProcessingState(processing) { + contextState.processing = processing; + } + + contextState.setModified = setModifiedState; + contextState.setSubmitted = setSubmittedState; + contextState.setProcessing = setProcessingState; + }, [contextState]); + useThrottledEffect(() => { refreshToken(); }, 15000, [fields]); useEffect(() => { setModified(false); - }, [locale]); + }, [locale, setModified]); const classes = [ className, baseClass, ].filter(Boolean).join(' '); - console.log('test'); - return (
{ action={action} className={classes} > - + ({ + processing: false, + modified: false, + submitted: false, + getFields: () => { }, + getField: () => { }, + getData: () => { }, + getSiblingData: () => { }, + getDataByPath: () => undefined, + getUnflattenedValues: () => { }, + validateForm: () => { }, + createFormData: () => { }, + submit: () => { }, + dispatchFields, + setModified: () => { }, +}); diff --git a/src/client/components/forms/field-types/Email/index.js b/src/client/components/forms/field-types/Email/index.js index 4dc8b864de..7daf5808ea 100644 --- a/src/client/components/forms/field-types/Email/index.js +++ b/src/client/components/forms/field-types/Email/index.js @@ -31,12 +31,7 @@ const Email = (props) => { return validationResult; }, [validate, required]); - const { - value, - showError, - setValue, - errorMessage, - } = useFieldType({ + const fieldType = useFieldType({ path, required, initialData, @@ -45,6 +40,13 @@ const Email = (props) => { enableDebouncedValue: true, }); + const { + value, + showError, + setValue, + errorMessage, + } = fieldType; + const classes = [ 'field-type', 'email', diff --git a/src/client/components/forms/useFieldType/index.js b/src/client/components/forms/useFieldType/index.js index 13d52bcde0..90ebf2a885 100644 --- a/src/client/components/forms/useFieldType/index.js +++ b/src/client/components/forms/useFieldType/index.js @@ -23,6 +23,7 @@ const useFieldType = (options) => { const initialData = data !== undefined ? data : defaultValue; const formContext = useContext(FormContext); + const { dispatchFields, submitted, processing, getField, setModified, modified, } = formContext; diff --git a/src/client/components/forms/withCondition/index.js b/src/client/components/forms/withCondition/index.js index 7d8fea2d21..97360caf41 100644 --- a/src/client/components/forms/withCondition/index.js +++ b/src/client/components/forms/withCondition/index.js @@ -4,23 +4,41 @@ import PropTypes from 'prop-types'; import useForm from '../Form/useForm'; const withCondition = (Field) => { + const CheckForCondition = (props) => { + const { condition } = props; + + if (condition) { + return ; + } + + return ; + }; + + CheckForCondition.defaultProps = { + condition: null, + name: '', + path: '', + }; + + CheckForCondition.propTypes = { + condition: PropTypes.func, + name: PropTypes.string, + path: PropTypes.string, + }; + const WithCondition = (props) => { const { condition, name, path } = props; const { getData, getSiblingData } = useForm(); - if (condition) { - const fields = getData(); - const siblingFields = getSiblingData(path || name); - const passesCondition = condition ? condition(fields, siblingFields) : true; + const fields = getData(); + const siblingFields = getSiblingData(path || name); + const passesCondition = condition ? condition(fields, siblingFields) : true; - if (passesCondition) { - return ; - } - - return null; + if (passesCondition) { + return ; } - return ; + return null; }; WithCondition.defaultProps = { @@ -35,7 +53,7 @@ const withCondition = (Field) => { path: PropTypes.string, }; - return WithCondition; + return CheckForCondition; }; export default withCondition;