From ade301cef5a2a28095484cbe39695ebd6895d9df Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 21 Oct 2024 10:47:22 +0200 Subject: [PATCH] chore: memoize properly in the stepper Signed-off-by: blam --- .changeset/heavy-mice-raise.md | 5 + .../src/next/components/Stepper/Stepper.tsx | 97 +++++++++++-------- .../next/hooks/useTransformSchemaToProps.ts | 37 +++---- 3 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 .changeset/heavy-mice-raise.md diff --git a/.changeset/heavy-mice-raise.md b/.changeset/heavy-mice-raise.md new file mode 100644 index 0000000000..4eef00d756 --- /dev/null +++ b/.changeset/heavy-mice-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix issue with `Stepper` causing additional re-renders, and not `memoizing` properly diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 809291a9f7..2e3eab5c16 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -163,54 +163,63 @@ export const Stepper = (stepperProps: StepperProps) => { }); }, [steps, activeStep, validators, apiHolder]); - const handleBack = () => { + const handleBack = useCallback(() => { setActiveStep(prevActiveStep => prevActiveStep - 1); - }; + }, [setActiveStep]); - const handleChange = (e: IChangeEvent) => { - setStepsState(current => { - const newState = [...current]; - newState[activeStep] = { - ...e.formData, - }; - return newState; - }); - }; - - const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); - - const handleNext = async ({ - formData = {}, - }: { - formData?: Record; - }) => { - // The validation should never throw, as the validators are wrapped in a try/catch. - // This makes it fine to set and unset state without try/catch. - setErrors(undefined); - setIsValidating(true); - - const returnedValidation = await validation(formData); - - setIsValidating(false); - - if (hasErrors(returnedValidation)) { - setErrors(returnedValidation); - } else { + const handleChange = useCallback( + (e: IChangeEvent) => { setStepsState(current => { const newState = [...current]; newState[activeStep] = { - ...formData, + ...e.formData, }; return newState; }); + }, + [activeStep, setStepsState], + ); + + const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); + + const handleNext = useCallback( + async ({ formData = {} }: { formData?: Record }) => { + // The validation should never throw, as the validators are wrapped in a try/catch. + // This makes it fine to set and unset state without try/catch. setErrors(undefined); - setActiveStep(prevActiveStep => { - const stepNum = prevActiveStep + 1; - analytics.captureEvent('click', `Next Step (${stepNum})`); - return stepNum; - }); - } - }; + setIsValidating(true); + + const returnedValidation = await validation(formData); + + setIsValidating(false); + + if (hasErrors(returnedValidation)) { + setErrors(returnedValidation); + } else { + setStepsState(current => { + const newState = [...current]; + newState[activeStep] = { + ...formData, + }; + return newState; + }); + setErrors(undefined); + setActiveStep(prevActiveStep => { + const stepNum = prevActiveStep + 1; + analytics.captureEvent('click', `Next Step (${stepNum})`); + return stepNum; + }); + } + }, + [ + activeStep, + validation, + analytics, + setActiveStep, + setErrors, + setStepsState, + ], + ); const { formContext: propFormContext, @@ -224,6 +233,14 @@ export const Stepper = (stepperProps: StepperProps) => { return { ...acc, ...step }; }, {}); + const formData = useMemo( + () => stepsState[activeStep], + // stepsState is recreated on every render, so we cache formData + // using the stringified version instead. + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(stepsState[activeStep])], + ); + const handleCreate = useCallback(() => { props.onCreate(formState); analytics.captureEvent('click', `${createLabel}`); @@ -265,7 +282,7 @@ export const Stepper = (stepperProps: StepperProps) => { key={activeStep} validator={validator} extraErrors={errors as unknown as ErrorSchema} - formData={{ ...stepsState[activeStep] }} + formData={formData} formContext={{ ...propFormContext, formData: formState }} schema={currentStep.schema} uiSchema={mergedUiSchema} diff --git a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts index 1e714eb874..b3c8e83431 100644 --- a/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts +++ b/plugins/scaffolder-react/src/next/hooks/useTransformSchemaToProps.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useMemo } from 'react'; import { LayoutOptions } from '../../layouts'; import { type ParsedTemplateSchema } from './useTemplateSchema'; @@ -28,24 +29,24 @@ export const useTransformSchemaToProps = ( const objectFieldTemplate = step?.uiSchema['ui:ObjectFieldTemplate'] as | string | undefined; + return useMemo(() => { + if (typeof objectFieldTemplate !== 'string') { + return step; + } - if (typeof objectFieldTemplate !== 'string') { - return step; - } + const Layout = layouts.find( + layout => layout.name === objectFieldTemplate, + )?.component; - const Layout = layouts.find( - layout => layout.name === objectFieldTemplate, - )?.component; - - if (!Layout) { - return step; - } - - return { - ...step, - uiSchema: { - ...step.uiSchema, - ['ui:ObjectFieldTemplate']: Layout, - }, - }; + if (!Layout) { + return step; + } + return { + ...step, + uiSchema: { + ...step.uiSchema, + ['ui:ObjectFieldTemplate']: Layout, + }, + }; + }, [layouts, objectFieldTemplate, step]); };