chore: memoize properly in the stepper

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-10-21 10:47:22 +02:00
parent 5cbd900606
commit ade301cef5
3 changed files with 81 additions and 58 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-react': patch
---
Fix issue with `Stepper` causing additional re-renders, and not `memoizing` properly
@@ -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<string, JsonValue>;
}) => {
// 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<string, JsonValue> }) => {
// 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}
@@ -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]);
};