Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form Builder: Improve DnD performance #923

Merged
merged 1 commit into from Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/form-builder/src/FormBuilder.tsx
Expand Up @@ -4,7 +4,7 @@ import classNames from 'classnames';
import { DragDropContext, Droppable } from '@eventespresso/adapters';
import { Container } from '@eventespresso/ui-components';

import { FormSection } from './FormSection';
import { FormSections } from './FormSection';
import { withFormState } from './context';
import { useFormState, useHandleDnD } from './state';
import { SECTIONS_DROPPABLE_ID } from './constants';
Expand Down Expand Up @@ -42,9 +42,7 @@ const FormBuilder: React.FC<FormBuilderProps> = ({ bodyClassName, containerClass

return (
<div {...droppableProps} className={className} ref={innerRef}>
{getSections().map((formSection, index) => (
<FormSection key={formSection.UUID} formSection={formSection} index={index} />
))}
<FormSections formSections={getSections()} />
{placeholder}
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions packages/form-builder/src/FormElement/FormElement.tsx
@@ -1,6 +1,8 @@
import { memo } from 'react';
import classNames from 'classnames';

import { Draggable } from '@eventespresso/adapters';
import { getPropsAreEqual } from '@eventespresso/utils';

import { FormElementInput } from './FormElementInput';
import { FormElementToolbar } from './FormElementToolbar';
Expand All @@ -9,7 +11,7 @@ import { useFormState } from '../state';

import type { FormElementProps } from '../types';

export const FormElement: React.FC<FormElementProps> = ({ element, index }) => {
export const FormElement = memo<FormElementProps>(({ element, index }) => {
const { isElementOpen } = useFormState();
const active = isElementOpen({ UUID: element.UUID });
const wrapperClass = classNames('ee-form-element__wrapper', active && 'ee-form-element__wrapper--active');
Expand All @@ -30,4 +32,4 @@ export const FormElement: React.FC<FormElementProps> = ({ element, index }) => {
}}
</Draggable>
);
};
}, getPropsAreEqual([['element'], ['index']]));
8 changes: 4 additions & 4 deletions packages/form-builder/src/FormElement/FormElementInput.tsx
@@ -1,13 +1,13 @@
import { useMemo } from 'react';
import { memo, useMemo } from 'react';

import { FormControl, FormHelperText } from '@eventespresso/adapters';
import type { AnyObject } from '@eventespresso/utils';
import { AnyObject, getPropsAreEqual } from '@eventespresso/utils';

import { MappedElement } from './MappedElement';
import type { FormElementProps } from '../types';
import { useUpdateElement } from './useUpdateElement';

export const FormElementInput: React.FC<FormElementProps> = ({ element }) => {
export const FormElementInput = memo<FormElementProps>(({ element }) => {
const onChangeValue = useUpdateElement(element);

const props = useMemo(() => {
Expand Down Expand Up @@ -82,4 +82,4 @@ export const FormElementInput: React.FC<FormElementProps> = ({ element }) => {
{element.helpText && <FormHelperText>{element.helpText}</FormHelperText>}
</FormControl>
);
};
}, getPropsAreEqual([['element']]));
7 changes: 4 additions & 3 deletions packages/form-builder/src/FormElement/FormElementToolbar.tsx
@@ -1,15 +1,16 @@
import { useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';

import { __ } from '@eventespresso/i18n';
import { Copy, DragHandle, SettingsOutlined, Trash } from '@eventespresso/icons';
import { IconButton, ButtonProps, ConfirmDelete } from '@eventespresso/ui-components';
import { getPropsAreEqual } from '@eventespresso/utils';

import { ELEMENT_BLOCKS_INDEXED } from '../constants';
import { useFormState } from '../state';

import type { FormElementToolbarProps } from '../types';

export const FormElementToolbar: React.FC<FormElementToolbarProps> = ({ element, dragHandleProps }) => {
export const FormElementToolbar = memo<FormElementToolbarProps>(({ element, dragHandleProps }) => {
const { copyElement, deleteElement, isElementOpen, toggleOpenElement } = useFormState();
const { UUID } = element;
const active = isElementOpen({ UUID });
Expand Down Expand Up @@ -72,4 +73,4 @@ export const FormElementToolbar: React.FC<FormElementToolbarProps> = ({ element,
</div>
</div>
);
};
}, getPropsAreEqual([['element']]));
@@ -1,14 +1,17 @@
import { memo } from 'react';

import { __ } from '@eventespresso/i18n';
import { Check, CheckList, Palette, SettingsOutlined } from '@eventespresso/icons';
import { Collapsible, Tabs, TabList, TabPanels, Tab, TabPanel } from '@eventespresso/ui-components';
import { getPropsAreEqual } from '@eventespresso/utils';

import type { FormElementProps } from '../../types';
import { Settings } from './Settings';
import { Styles } from './Styles';
import { Validation } from './Validation';
import { useFormState } from '../../state';

export const FormElementTabs: React.FC<FormElementProps> = ({ element }) => {
export const FormElementTabs = memo<FormElementProps>(({ element }) => {
const { isElementOpen } = useFormState();
return (
<Collapsible show={isElementOpen({ UUID: element.UUID })}>
Expand Down Expand Up @@ -48,4 +51,4 @@ export const FormElementTabs: React.FC<FormElementProps> = ({ element }) => {
</Tabs>
</Collapsible>
);
};
}, getPropsAreEqual([['element']]));
6 changes: 4 additions & 2 deletions packages/form-builder/src/FormSection/FormSection.tsx
@@ -1,6 +1,8 @@
import { memo } from 'react';
import classNames from 'classnames';

import { Draggable } from '@eventespresso/adapters';
import { getPropsAreEqual } from '@eventespresso/utils';

import { AddFormElementPopover } from './AddFormElementPopover';
import { FormSectionToolbar } from './FormSectionToolbar';
Expand All @@ -10,7 +12,7 @@ import { useFormState } from '../state';

import type { FormSectionProps } from '../types';

export const FormSection: React.FC<FormSectionProps> = ({ formSection, index }) => {
export const FormSection = memo<FormSectionProps>(({ formSection, index }) => {
const { isElementOpen } = useFormState();

const { UUID } = formSection;
Expand All @@ -36,4 +38,4 @@ export const FormSection: React.FC<FormSectionProps> = ({ formSection, index })
}}
</Draggable>
);
};
}, getPropsAreEqual([['formSection'], ['index']]));
6 changes: 4 additions & 2 deletions packages/form-builder/src/FormSection/FormSectionElements.tsx
@@ -1,12 +1,14 @@
import { memo } from 'react';
import classNames from 'classnames';

import { Droppable } from '@eventespresso/adapters';
import { getPropsAreEqual } from '@eventespresso/utils';

import { useFormState } from '../state';
import type { FormSectionProps } from '../types';
import { FormElement } from '../FormElement';

export const FormSectionElements: React.FC<FormSectionProps> = ({ formSection }) => {
export const FormSectionElements = memo<FormSectionProps>(({ formSection }) => {
const { getElements } = useFormState();

return (
Expand All @@ -25,4 +27,4 @@ export const FormSectionElements: React.FC<FormSectionProps> = ({ formSection })
}}
</Droppable>
);
};
}, getPropsAreEqual([['formSection']]));
7 changes: 4 additions & 3 deletions packages/form-builder/src/FormSection/FormSectionToolbar.tsx
@@ -1,15 +1,16 @@
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, memo } from 'react';

import { __ } from '@eventespresso/i18n';
import { Copy, DragHandle, SettingsOutlined, Trash } from '@eventespresso/icons';
import { IconButton, ButtonProps, ConfirmDelete } from '@eventespresso/ui-components';
import { getPropsAreEqual } from '@eventespresso/utils';

import { useFormState } from '../state';
import { SaveSection } from './SaveSection';

import type { FormSectionToolbarProps } from '../types';

export const FormSectionToolbar: React.FC<FormSectionToolbarProps> = ({ formSection, dragHandleProps }) => {
export const FormSectionToolbar = memo<FormSectionToolbarProps>(({ formSection, dragHandleProps }) => {
const { copySection, deleteSection, isElementOpen, toggleOpenElement } = useFormState();

const { UUID } = formSection;
Expand Down Expand Up @@ -72,4 +73,4 @@ export const FormSectionToolbar: React.FC<FormSectionToolbarProps> = ({ formSect
</div>
</div>
);
};
}, getPropsAreEqual([['formSection']]));
21 changes: 21 additions & 0 deletions packages/form-builder/src/FormSection/FormSections.tsx
@@ -0,0 +1,21 @@
import { memo } from 'react';

import { getPropsAreEqual } from '@eventespresso/utils';

import { FormSection } from './FormSection';

import type { FormSection as TFormSection } from '../types';

export type FormSectionsProps = {
formSections: Array<TFormSection>;
};

export const FormSections: React.FC<FormSectionsProps> = memo(({ formSections }) => {
return (
<>
{formSections.map((formSection, index) => (
<FormSection key={formSection.UUID} formSection={formSection} index={index} />
))}
</>
);
}, getPropsAreEqual([['formSections']]));
Comment on lines +13 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool cool

@@ -1,13 +1,16 @@
import { memo } from 'react';

import { __ } from '@eventespresso/i18n';
import { CheckList, Palette, SettingsOutlined } from '@eventespresso/icons';
import { Collapsible, Tabs, TabList, TabPanels, Tab, TabPanel } from '@eventespresso/ui-components';
import { getPropsAreEqual } from '@eventespresso/utils';

import { useFormState } from '../../state';
import { Styles } from './Styles';
import type { FormSectionProps } from '../../types';
import { Settings } from './Settings';

export const FormSectionTabs: React.FC<FormSectionProps> = ({ formSection }) => {
export const FormSectionTabs = memo<FormSectionProps>(({ formSection }) => {
const { isElementOpen } = useFormState();
return (
<Collapsible show={isElementOpen({ UUID: formSection.UUID })}>
Expand Down Expand Up @@ -40,4 +43,4 @@ export const FormSectionTabs: React.FC<FormSectionProps> = ({ formSection }) =>
</Tabs>
</Collapsible>
);
};
}, getPropsAreEqual([['formSection']]));
2 changes: 1 addition & 1 deletion packages/form-builder/src/FormSection/index.ts
@@ -1 +1 @@
export * from './FormSection';
export * from './FormSections';
4 changes: 2 additions & 2 deletions packages/form-builder/src/state/useFormStateManager.ts
Expand Up @@ -22,7 +22,7 @@ export const useFormStateManager: FormStateManagerHook = (props) => {
const getSections = useCallback<FSM['getSections']>(() => {
const sections = Object.values(state.sections).filter(isNotSharedOrDefault);
return sortByOrder(sections);
}, [state]);
}, [state.sections]);

const getElements = useCallback<FSM['getElements']>(
({ sectionId }) => {
Expand All @@ -32,7 +32,7 @@ export const useFormStateManager: FormStateManagerHook = (props) => {
}
return sortByOrder(elements);
},
[state]
[state.elements]
);

const addSection = useCallback<FSM['addSection']>(({ section, afterUuid }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/form-builder/src/state/useFormStateReducer.ts
Expand Up @@ -20,6 +20,7 @@ export const initialState: FormState = {
deletedElements: [],
deletedSections: [],
isDirty: false,
openElement: '',
};

export const useFormStateReducer = (initializer: StateInitializer): FormStateReducer => {
Expand Down
14 changes: 9 additions & 5 deletions packages/utils/src/memo/getPropsAreEqual.ts
@@ -1,21 +1,27 @@
import { path, Path } from 'ramda';

import { isEqualJson } from '../misc';
import type { AnyObject } from '../types';

type PropsAreEqual<P extends AnyObject> = (
export type PropsAreEqual<P extends AnyObject> = (
prevProps: Readonly<React.PropsWithChildren<P>>,
nextProps: Readonly<React.PropsWithChildren<P>>
) => boolean;

/**
* Generates the comparison function that can be used as second argument to React.memo()
*/
const getPropsAreEqual = <P extends AnyObject>(...paths: Array<Path>): PropsAreEqual<P> => {
export const getPropsAreEqual = <P extends AnyObject>(paths: Array<Path>, compareAsJson = true): PropsAreEqual<P> => {
const propsAreEqual: PropsAreEqual<P> = (prevProps, nextProps): boolean => {
for (const pathToValue of paths) {
const prevValue = path<any>(pathToValue, prevProps);
const nextValue = path<any>(pathToValue, nextProps);

if (prevValue !== nextValue) {
if (compareAsJson) {
if (!isEqualJson(prevValue, nextValue)) {
return false;
}
} else if (prevValue !== nextValue) {
return false;
}
}
Expand All @@ -24,5 +30,3 @@ const getPropsAreEqual = <P extends AnyObject>(...paths: Array<Path>): PropsAreE

return propsAreEqual;
};

export default getPropsAreEqual;
2 changes: 1 addition & 1 deletion packages/utils/src/memo/index.ts
@@ -1,2 +1,2 @@
export { default as entityListCacheIdString } from './entityListCacheIdString';
export { default as getPropsAreEqual } from './getPropsAreEqual';
export * from './getPropsAreEqual';