Skip to content

Commit 0b9e40b

Browse files
committed
Optimize React components with memo, useMemo, useCallback
- FormBuilder: Fix state mutation, memoize expensive computations - Card: Add React.memo, useCallback for event handlers - Section: Add React.memo, useMemo, useCallback throughout - CardModal: Add React.memo, useCallback for handlers - Add: Add React.memo, useCallback for handlers - CardEnumOptions: Fix module-level ID counter, add React.memo - CardGeneralParameterInputs: Add React.memo, memoize functions - PredefinedGallery: Fix state mutation, add memoization - Collapse, FBCheckbox, FBRadioGroup, FBRadioButton: Add React.memo - PlaygroundContainer: Add useCallback for onChange All components now follow React best practices: - Immutable state updates - Memoized expensive computations - Stable callback references - Pure functional components wrapped in memo()
1 parent e857f97 commit 0b9e40b

File tree

13 files changed

+728
-406
lines changed

13 files changed

+728
-406
lines changed

example/components/PlaygroundContainer.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useCallback, useMemo } from 'react';
22

33
import JsonSchemaFormSuite from './JsonSchemaFormSuite';
44
import Box from '@mui/material/Box';
@@ -45,10 +45,19 @@ const theme = createTheme({
4545
});
4646

4747
export default function PlaygroundContainer({ title }: { title: string }) {
48-
const [schema, setSchema] = React.useState(JSON.stringify(initialJsonSchema));
49-
const [uischema, setUischema] = React.useState(
48+
const [schema, setSchema] = useState(() => JSON.stringify(initialJsonSchema));
49+
const [uischema, setUischema] = useState(() =>
5050
JSON.stringify(initialUiSchema),
5151
);
52+
53+
const handleChange = useCallback(
54+
(newSchema: string, newUiSchema: string) => {
55+
setSchema(newSchema);
56+
setUischema(newUiSchema);
57+
},
58+
[],
59+
);
60+
5261
return (
5362
<ThemeProvider theme={theme}>
5463
<CssBaseline />
@@ -111,10 +120,7 @@ export default function PlaygroundContainer({ title }: { title: string }) {
111120
mods={mods}
112121
schemaTitle="Data Schema"
113122
uischemaTitle="UI Schema"
114-
onChange={(newSchema: string, newUiSchema: string) => {
115-
setSchema(newSchema);
116-
setUischema(newUiSchema);
117-
}}
123+
onChange={handleChange}
118124
width="100%"
119125
height="750px"
120126
/>

src/formBuilder/Add.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, ReactElement } from 'react';
1+
import React, { useState, ReactElement, useCallback, memo } from 'react';
22
import Popover from '@mui/material/Popover';
33
import Box from '@mui/material/Box';
44
import Stack from '@mui/material/Stack';
@@ -11,7 +11,7 @@ import FBRadioGroup from './radio/FBRadioGroup';
1111
import { getRandomId } from './utils';
1212
import type { ModLabels } from './types';
1313

14-
export default function Add({
14+
function Add({
1515
addElem,
1616
hidden,
1717
tooltipDescription,
@@ -24,16 +24,36 @@ export default function Add({
2424
}): ReactElement {
2525
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
2626
const [createChoice, setCreateChoice] = useState('card');
27-
const [elementId] = useState(getRandomId());
27+
const [elementId] = useState(getRandomId);
2828
const [isHovered, setIsHovered] = useState(false);
2929

30-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
31-
setAnchorEl(event.currentTarget);
32-
};
30+
const handleClick = useCallback(
31+
(event: React.MouseEvent<HTMLButtonElement>) => {
32+
setAnchorEl(event.currentTarget);
33+
},
34+
[],
35+
);
3336

34-
const handleClose = () => {
37+
const handleClose = useCallback(() => {
3538
setAnchorEl(null);
36-
};
39+
}, []);
40+
41+
const handleMouseEnter = useCallback(() => {
42+
setIsHovered(true);
43+
}, []);
44+
45+
const handleMouseLeave = useCallback(() => {
46+
setIsHovered(false);
47+
}, []);
48+
49+
const handleChoiceChange = useCallback((selection: string) => {
50+
setCreateChoice(selection);
51+
}, []);
52+
53+
const handleCreate = useCallback(() => {
54+
addElem(createChoice);
55+
setAnchorEl(null);
56+
}, [addElem, createChoice]);
3757

3858
const open = Boolean(anchorEl);
3959

@@ -48,8 +68,8 @@ export default function Add({
4868
py: 1,
4969
my: 0.5,
5070
}}
51-
onMouseEnter={() => setIsHovered(true)}
52-
onMouseLeave={() => setIsHovered(false)}
71+
onMouseEnter={handleMouseEnter}
72+
onMouseLeave={handleMouseLeave}
5373
>
5474
<Tooltip title={tooltipDescription || 'Add element here'} placement='top'>
5575
<IconButton
@@ -101,9 +121,7 @@ export default function Add({
101121
label: labels?.addSectionLabel ?? 'Form section',
102122
},
103123
]}
104-
onChange={(selection) => {
105-
setCreateChoice(selection);
106-
}}
124+
onChange={handleChoiceChange}
107125
/>
108126
<Stack
109127
direction='row'
@@ -120,10 +138,7 @@ export default function Add({
120138
Cancel
121139
</Button>
122140
<Button
123-
onClick={() => {
124-
addElem(createChoice);
125-
handleClose();
126-
}}
141+
onClick={handleCreate}
127142
variant='contained'
128143
color='primary'
129144
size='small'
@@ -136,3 +151,5 @@ export default function Add({
136151
</Box>
137152
);
138153
}
154+
155+
export default memo(Add);

src/formBuilder/Card.tsx

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactElement } from 'react';
1+
import React, { ReactElement, memo, useCallback, useState } from 'react';
22
import Box from '@mui/material/Box';
33
import Stack from '@mui/material/Stack';
44
import Tooltip from '@mui/material/Tooltip';
@@ -16,7 +16,7 @@ import TooltipComponent from './Tooltip';
1616
import { getRandomId } from './utils';
1717
import type { CardPropsType, CardComponentPropsType } from './types';
1818

19-
export default function Card({
19+
function Card({
2020
componentProps,
2121
onChange,
2222
onDelete,
@@ -31,19 +31,72 @@ export default function Card({
3131
showObjectNameInput = true,
3232
addProperties,
3333
}: CardPropsType): ReactElement {
34-
const [modalOpen, setModalOpen] = React.useState(false);
35-
const [elementId] = React.useState(getRandomId());
34+
const [modalOpen, setModalOpen] = useState(false);
35+
const [elementId] = useState(getRandomId);
36+
37+
const handleToggleCollapse = useCallback(() => {
38+
setCardOpen(!cardOpen);
39+
}, [cardOpen, setCardOpen]);
40+
41+
const handleModalOpen = useCallback(() => {
42+
setModalOpen(true);
43+
}, []);
44+
45+
const handleModalClose = useCallback(() => {
46+
setModalOpen(false);
47+
}, []);
48+
49+
const handleRequiredToggle = useCallback(() => {
50+
onChange({
51+
...componentProps,
52+
required: !componentProps.required,
53+
});
54+
}, [componentProps, onChange]);
55+
56+
const handleMoveUp = useCallback(
57+
(e: React.MouseEvent) => {
58+
e.stopPropagation();
59+
onMoveUp?.();
60+
},
61+
[onMoveUp],
62+
);
63+
64+
const handleMoveDown = useCallback(
65+
(e: React.MouseEvent) => {
66+
e.stopPropagation();
67+
onMoveDown?.();
68+
},
69+
[onMoveDown],
70+
);
71+
72+
const handleDelete = useCallback(() => {
73+
onDelete?.();
74+
}, [onDelete]);
75+
76+
const handleModalSave = useCallback(
77+
(newComponentProps: CardComponentPropsType) => {
78+
onChange(newComponentProps);
79+
},
80+
[onChange],
81+
);
82+
83+
const handleAddElem = useCallback(
84+
(choice: string) => {
85+
addElem?.(choice);
86+
},
87+
[addElem],
88+
);
3689

3790
return (
3891
<React.Fragment>
3992
<Collapse
4093
isOpen={cardOpen}
41-
toggleCollapse={() => setCardOpen(!cardOpen)}
94+
toggleCollapse={handleToggleCollapse}
4295
title={
4396
<React.Fragment>
4497
<Box
4598
component='span'
46-
onClick={() => setCardOpen(!cardOpen)}
99+
onClick={handleToggleCollapse}
47100
sx={{
48101
cursor: 'pointer',
49102
display: 'flex',
@@ -71,10 +124,7 @@ export default function Card({
71124
<Tooltip title='Move form element up' placement='top'>
72125
<IconButton
73126
size='small'
74-
onClick={(e) => {
75-
e.stopPropagation();
76-
onMoveUp?.();
77-
}}
127+
onClick={handleMoveUp}
78128
id={`${elementId}_moveupbiginfo`}
79129
aria-label='Move form element up'
80130
>
@@ -84,10 +134,7 @@ export default function Card({
84134
<Tooltip title='Move form element down' placement='top'>
85135
<IconButton
86136
size='small'
87-
onClick={(e) => {
88-
e.stopPropagation();
89-
onMoveDown?.();
90-
}}
137+
onClick={handleMoveDown}
91138
id={`${elementId}_movedownbiginfo`}
92139
aria-label='Move form element down'
93140
>
@@ -116,7 +163,7 @@ export default function Card({
116163
>
117164
<IconButton
118165
size='small'
119-
onClick={() => setModalOpen(true)}
166+
onClick={handleModalOpen}
120167
id={`${elementId}_editinfo`}
121168
color='primary'
122169
aria-label='Additional configurations'
@@ -127,7 +174,7 @@ export default function Card({
127174
<Tooltip title='Delete form element' placement='top'>
128175
<IconButton
129176
size='small'
130-
onClick={() => onDelete?.()}
177+
onClick={handleDelete}
131178
id={`${elementId}_trashinfo`}
132179
color='error'
133180
aria-label='Delete form element'
@@ -146,12 +193,7 @@ export default function Card({
146193
}}
147194
>
148195
<FBCheckbox
149-
onChangeValue={() =>
150-
onChange({
151-
...componentProps,
152-
required: !componentProps.required,
153-
})
154-
}
196+
onChangeValue={handleRequiredToggle}
155197
isChecked={!!componentProps.required}
156198
label='Required'
157199
id={`${elementId}_required`}
@@ -161,20 +203,20 @@ export default function Card({
161203
<CardModal
162204
componentProps={componentProps as CardComponentPropsType}
163205
isOpen={modalOpen}
164-
onClose={() => setModalOpen(false)}
165-
onChange={(newComponentProps: CardComponentPropsType) => {
166-
onChange(newComponentProps);
167-
}}
206+
onClose={handleModalClose}
207+
onChange={handleModalSave}
168208
TypeSpecificParameters={TypeSpecificParameters}
169209
/>
170210
</Collapse>
171211
{mods?.components?.add && mods?.components?.add(addProperties)}
172212
{!mods?.components?.add && addElem && (
173213
<Add
174214
tooltipDescription={((mods || {}).tooltipDescriptions || {}).add}
175-
addElem={(choice: string) => addElem(choice)}
215+
addElem={handleAddElem}
176216
/>
177217
)}
178218
</React.Fragment>
179219
);
180220
}
221+
222+
export default memo(Card);

0 commit comments

Comments
 (0)