further separates contexts within Form for performance reasons

This commit is contained in:
James
2020-07-07 21:41:34 -04:00
parent 0930063219
commit ac5caf4da2
21 changed files with 156 additions and 144 deletions

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import config from 'payload/config';
import Button from '../Button';
import useForm from '../../forms/Form/useForm';
import { useForm } from '../../forms/Form/context';
import './index.scss';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import useForm from '../../forms/Form/useForm';
import { useForm } from '../../forms/Form/context';
import { useUser } from '../../data/User';
import Button from '../Button';

View File

@@ -1,3 +0,0 @@
import { createContext } from 'react';
export default createContext({});

View File

@@ -1,3 +0,0 @@
import { createContext } from 'react';
export default createContext({});

View File

@@ -0,0 +1,26 @@
import { createContext, useContext } from 'react';
const FormContext = createContext({});
const FieldContext = createContext({});
const SubmittedContext = createContext(false);
const ProcessingContext = createContext(false);
const ModifiedContext = createContext(false);
const useForm = () => useContext(FormContext);
const useFormFields = () => useContext(FieldContext);
const useFormSubmitted = () => useContext(SubmittedContext);
const useFormProcessing = () => useContext(ProcessingContext);
const useFormModified = () => useContext(ModifiedContext);
export {
FormContext,
FieldContext,
SubmittedContext,
ProcessingContext,
ModifiedContext,
useForm,
useFormFields,
useFormSubmitted,
useFormProcessing,
useFormModified,
};

View File

@@ -1,13 +1,11 @@
import React, {
useReducer, useEffect, useRef,
useReducer, useEffect, useRef, useState,
} from 'react';
import { objectToFormData } from 'object-to-formdata';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { unflatten } from 'flatley';
import HiddenInput from '../field-types/HiddenInput';
import FormContext from './FormContext';
import FieldContext from './FieldContext';
import { useLocale } from '../../utilities/Locale';
import { useStatusList } from '../../elements/Status';
import { requests } from '../../../api';
@@ -16,6 +14,8 @@ import { useUser } from '../../data/User';
import fieldReducer from './fieldReducer';
import initContextState from './initContextState';
import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context';
import './index.scss';
const baseClass = 'form';
@@ -43,7 +43,7 @@ const Form = (props) => {
ajax,
method,
action,
handleAjaxResponse,
handleResponse,
onSuccess,
children,
className,
@@ -56,6 +56,10 @@ const Form = (props) => {
const { replaceStatus, addStatus, clearStatus } = useStatusList();
const { refreshCookie } = useUser();
const [modified, setModified] = useState(false);
const [processing, setProcessing] = useState(false);
const [submitted, setSubmitted] = useState(false);
const contextRef = useRef({ ...initContextState });
const [fields, dispatchFields] = useReducer(fieldReducer, {});
@@ -69,7 +73,7 @@ const Form = (props) => {
}
e.stopPropagation();
contextRef.current.setSubmitted(true);
setSubmitted(true);
const isValid = contextRef.current.validateForm();
@@ -106,17 +110,17 @@ const Form = (props) => {
});
const formData = contextRef.current.createFormData();
contextRef.current.setProcessing(true);
setProcessing(true);
// Make the API call from the action
return requests[method.toLowerCase()](action, {
body: formData,
}).then((res) => {
contextRef.current.setModified(false);
if (typeof handleAjaxResponse === 'function') return handleAjaxResponse(res);
setModified(false);
if (typeof handleResponse === 'function') return handleResponse(res);
return res.json().then((json) => {
contextRef.current.setProcessing(false);
setProcessing(false);
clearStatus();
if (res.status < 400) {
@@ -144,9 +148,7 @@ const Form = (props) => {
}
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]];
}, [[], []]);
const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]);
fieldErrors.forEach((err) => {
dispatchFields({
@@ -193,17 +195,11 @@ const Form = (props) => {
return true;
};
contextRef.current.getFields = () => {
return contextRef.current.fields;
};
contextRef.current.getFields = () => contextRef.current.fields;
contextRef.current.getField = (path) => {
return contextRef.current.fields[path];
};
contextRef.current.getField = (path) => contextRef.current.fields[path];
contextRef.current.getData = () => {
return reduceFieldsToValues(contextRef.current.fields, true);
};
contextRef.current.getData = () => reduceFieldsToValues(contextRef.current.fields, true);
contextRef.current.getSiblingData = (path) => {
let siblingFields = contextRef.current.fields;
@@ -247,40 +243,26 @@ const Form = (props) => {
return unflattenedData?.[name];
};
contextRef.current.getUnflattenedValues = () => {
return reduceFieldsToValues(contextRef.current.fields);
};
contextRef.current.getUnflattenedValues = () => reduceFieldsToValues(contextRef.current.fields);
contextRef.current.validateForm = () => {
return !Object.values(contextRef.current.fields).some((field) => {
return field.valid === false;
});
};
contextRef.current.validateForm = () => !Object.values(contextRef.current.fields).some((field) => field.valid === false);
contextRef.current.createFormData = () => {
const data = reduceFieldsToValues(contextRef.current.fields);
return objectToFormData(data, { indices: true });
};
contextRef.current.setModified = (modified) => {
contextRef.current.modified = modified;
};
contextRef.current.setSubmitted = (submitted) => {
contextRef.current.submitted = submitted;
};
contextRef.current.setProcessing = (processing) => {
contextRef.current.processing = processing;
};
contextRef.current.setModified = setModified;
contextRef.current.setProcessing = setProcessing;
contextRef.current.setSubmitted = setSubmitted;
useThrottledEffect(() => {
refreshCookie();
}, 15000, [fields]);
useEffect(() => {
contextRef.current.modified = false;
}, [locale, contextRef.current.modified]);
setModified(false);
}, [locale]);
const classes = [
className,
@@ -301,13 +283,20 @@ const Form = (props) => {
...contextRef.current,
}}
>
<HiddenInput
path="locale"
defaultValue={locale}
/>
{children}
<SubmittedContext.Provider value={submitted}>
<ProcessingContext.Provider value={processing}>
<ModifiedContext.Provider value={modified}>
<HiddenInput
path="locale"
defaultValue={locale}
/>
{children}
</ModifiedContext.Provider>
</ProcessingContext.Provider>
</SubmittedContext.Provider>
</FieldContext.Provider>
</FormContext.Provider>
</form>
);
};
@@ -318,7 +307,7 @@ Form.defaultProps = {
ajax: true,
method: 'POST',
action: '',
handleAjaxResponse: null,
handleResponse: null,
onSuccess: null,
className: '',
disableSuccessStatus: false,
@@ -331,7 +320,7 @@ Form.propTypes = {
ajax: PropTypes.bool,
method: PropTypes.oneOf(['post', 'POST', 'get', 'GET', 'put', 'PUT', 'delete', 'DELETE']),
action: PropTypes.string,
handleAjaxResponse: PropTypes.func,
handleResponse: PropTypes.func,
onSuccess: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),

View File

@@ -1,7 +1,4 @@
export default {
processing: false,
modified: false,
submitted: false,
getFields: () => { },
getField: () => { },
getData: () => { },

View File

@@ -1,4 +0,0 @@
import { useContext } from 'react';
import FormContext from './FormContext';
export default () => useContext(FormContext);

View File

@@ -1,4 +0,0 @@
import { useContext } from 'react';
import FieldContext from './FieldContext';
export default () => useContext(FieldContext);

View File

@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import FormContext from '../Form/FormContext';
import { useFormProcessing } from '../Form/context';
import Button from '../../elements/Button';
import './index.scss';
@@ -8,12 +8,13 @@ import './index.scss';
const baseClass = 'form-submit';
const FormSubmit = ({ children }) => {
const formContext = useContext(FormContext);
const processing = useFormProcessing();
return (
<div className={baseClass}>
<Button
type="submit"
disabled={formContext.processing ? true : undefined}
disabled={processing ? true : undefined}
>
{children}
</Button>

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import useFieldType from '../../useFieldType';
@@ -6,19 +6,21 @@ import Label from '../../Label';
import Button from '../../../elements/Button';
import CopyToClipboard from '../../../elements/CopyToClipboard';
import { text } from '../../../../../fields/validations';
import useFormFields from '../../Form/useFormFields';
import { useFormFields } from '../../Form/context';
import './index.scss';
const path = 'apiKey';
const baseClass = 'api-key';
const validate = val => text(val, { minLength: 24, maxLength: 48 });
const validate = (val) => text(val, { minLength: 24, maxLength: 48 });
const APIKey = (props) => {
const {
initialData,
} = props;
const [initialAPIKey, setInitialAPIKey] = useState(null);
const { getField } = useFormFields();
const apiKey = getField(path);
@@ -36,7 +38,7 @@ const APIKey = (props) => {
const fieldType = useFieldType({
path: 'apiKey',
initialData: initialData || uuidv4(),
initialData: initialData || initialAPIKey,
validate,
});
@@ -45,6 +47,10 @@ const APIKey = (props) => {
setValue,
} = fieldType;
useEffect(() => {
setInitialAPIKey(uuidv4());
}, []);
const classes = [
'field-type',
'api-key',
@@ -52,7 +58,7 @@ const APIKey = (props) => {
].filter(Boolean).join(' ');
return (
<>
<React.Fragment>
<div className={classes}>
<Label
htmlFor={path}
@@ -73,7 +79,7 @@ const APIKey = (props) => {
>
Generate new API Key
</Button>
</>
</React.Fragment>
);
};

View File

@@ -5,7 +5,7 @@ import Password from '../Password';
import Checkbox from '../Checkbox';
import Button from '../../../elements/Button';
import ConfirmPassword from '../ConfirmPassword';
import useFormFields from '../../Form/useFormFields';
import { useFormFields } from '../../Form/context';
import APIKey from './APIKey';
import './index.scss';
@@ -81,7 +81,11 @@ Auth.defaultProps = {
Auth.propTypes = {
fieldTypes: PropTypes.shape({}).isRequired,
initialData: PropTypes.shape({}),
initialData: PropTypes.shape({
enableAPIKey: PropTypes.bool,
apiKey: PropTypes.string,
email: PropTypes.string,
}),
useAPIKey: PropTypes.bool,
requirePassword: PropTypes.bool,
};

View File

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import Label from '../../Label';
import Error from '../../Error';
import useFormFields from '../../Form/useFormFields';
import { useFormFields } from '../../Form/context';
import './index.scss';

View File

@@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import reducer from '../rowReducer';
import useForm from '../../Form/useForm';
import { useForm } from '../../Form/context';
import DraggableSection from '../../DraggableSection';
import { useRenderedFields } from '../../RenderFields';
import Error from '../../Error';
@@ -139,7 +139,7 @@ const Flexible = (props) => {
</header>
<Droppable droppableId="flexible-drop">
{provided => (
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
@@ -151,7 +151,7 @@ const Flexible = (props) => {
blockType = dataToInitialize?.[i]?.blockType;
}
const blockToRender = blocks.find(block => block.slug === blockType);
const blockToRender = blocks.find((block) => block.slug === blockType);
if (blockToRender) {
return (
@@ -186,8 +186,7 @@ const Flexible = (props) => {
}
return null;
})
}
})}
{provided.placeholder}
</div>
)}

View File

@@ -28,9 +28,9 @@ const NumberField = (props) => {
const path = pathFromProps || name;
const memoizedValidate = useCallback((value) => {
const validationResult = validate(value, { min, max });
const validationResult = validate(value, { min, max, required });
return validationResult;
}, [validate, max, min]);
}, [validate, max, min, required]);
const {
value,
@@ -46,6 +46,12 @@ const NumberField = (props) => {
enableDebouncedValue: true,
});
const handleChange = useCallback((e) => {
let val = parseInt(e.target.value, 10);
if (Number.isNaN(val)) val = '';
setValue(val);
}, [setValue]);
const classes = [
'field-type',
'number',
@@ -72,7 +78,7 @@ const NumberField = (props) => {
/>
<input
value={value || ''}
onChange={e => setValue(parseInt(e.target.value, 10))}
onChange={handleChange}
disabled={readOnly ? 'disabled' : undefined}
placeholder={placeholder}
type="number"

View File

@@ -8,7 +8,7 @@ import Button from '../../../elements/Button';
import DraggableSection from '../../DraggableSection';
import reducer from '../rowReducer';
import { useRenderedFields } from '../../RenderFields';
import useForm from '../../Form/useForm';
import { useForm } from '../../Form/context';
import useFieldType from '../../useFieldType';
import Error from '../../Error';
import { repeater } from '../../../../../fields/validations';
@@ -124,35 +124,32 @@ const Repeater = (props) => {
/>
</header>
<Droppable droppableId="repeater-drop">
{provided => (
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{rows.length > 0 && rows.map((row, i) => {
return (
<DraggableSection
key={row.key}
id={row.key}
blockType="repeater"
singularLabel={singularLabel}
isOpen={row.open}
rowCount={rows.length}
rowIndex={i}
addRow={() => addRow(i)}
removeRow={() => removeRow(i)}
moveRow={moveRow}
parentPath={path}
initialData={row.data}
initNull={row.initNull}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldTypes={fieldTypes}
fieldSchema={fields}
permissions={permissions.fields}
/>
);
})
}
{rows.length > 0 && rows.map((row, i) => (
<DraggableSection
key={row.key}
id={row.key}
blockType="repeater"
singularLabel={singularLabel}
isOpen={row.open}
rowCount={rows.length}
rowIndex={i}
addRow={() => addRow(i)}
removeRow={() => removeRow(i)}
moveRow={moveRow}
parentPath={path}
initialData={row.data}
initNull={row.initNull}
customComponentsPath={`${customComponentsPath}${name}.fields.`}
fieldTypes={fieldTypes}
fieldSchema={fields}
permissions={permissions.fields}
/>
))}
{provided.placeholder}
</div>
)}

View File

@@ -1,7 +1,7 @@
import {
useContext, useCallback, useEffect, useState,
useCallback, useEffect, useState,
} from 'react';
import FormContext from '../Form/FormContext';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
import useDebounce from '../../../hooks/useDebounce';
import useUnmountEffect from '../../../hooks/useUnmountEffect';
@@ -23,10 +23,13 @@ const useFieldType = (options) => {
// If no initialData, use default value
const initialData = data !== undefined ? data : defaultValue;
const formContext = useContext(FormContext);
const formContext = useForm();
const submitted = useFormSubmitted();
const processing = useFormProcessing();
const modified = useFormModified();
const {
dispatchFields, submitted, processing, getField, setModified, modified,
dispatchFields, getField, setModified,
} = formContext;
const [internalValue, setInternalValue] = useState(initialData);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import useFormFields from '../Form/useFormFields';
import { useFormFields } from '../Form/context';
const withCondition = (Field) => {
const CheckForCondition = (props) => {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import NavigationPrompt from 'react-router-navigation-prompt';
import useForm from '../../forms/Form/useForm';
import { useForm } from '../../forms/Form/context';
import MinimalTemplate from '../../templates/Minimal';
import Button from '../../elements/Button';
@@ -13,27 +13,25 @@ const LeaveWithoutSaving = () => {
return (
<NavigationPrompt when={modified}>
{({ onConfirm, onCancel }) => {
return (
<div className={modalSlug}>
<MinimalTemplate>
<h1>Leave without saving</h1>
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p>
<Button
onClick={onCancel}
buttonStyle="secondary"
>
Stay on this page
</Button>
<Button
onClick={onConfirm}
>
Leave anyway
</Button>
</MinimalTemplate>
</div>
);
}}
{({ onConfirm, onCancel }) => (
<div className={modalSlug}>
<MinimalTemplate>
<h1>Leave without saving</h1>
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p>
<Button
onClick={onCancel}
buttonStyle="secondary"
>
Stay on this page
</Button>
<Button
onClick={onConfirm}
>
Leave anyway
</Button>
</MinimalTemplate>
</div>
)}
</NavigationPrompt>
);
};

View File

@@ -1,4 +1,4 @@
import useFormFields from '../components/forms/Form/useFormFields';
import { useFormFields } from '../components/forms/Form/context';
const useTitle = (useAsTitle) => {
const { getField } = useFormFields();