I’m developing a dynamic form with React Hook Form and Material-UI where the content changes based on the first two user selections. My setup includes a “Generator” page that renders different form configurations based on these selections. However, I’m encountering performance issues as the entire Generator component re-renders whenever an input value changes.
Here’s a simplified outline of my setup:
- formConfiguration: Always visible and does not change.
- combinedConfigurations: Changes based on initial user choices.
Generator: Renders forms based on the configuration.
Each input modification triggers a re-render of the Generator, as indicated by a console.log(‘generator’) call.
I have attempted using useMemo and useCallback to optimize performance but the issue persists.
Here’s my generator page
//all my import...
export const Generator = () => {
const dispatch = useDispatch();
const methods = useForm({
defaultValues: {
createdBy: 'User one',
},
});
const step = useSelector((state) => state.step.actual);
const customers = useSelector((state) => state.customer.customers);
const customerUsers = useSelector((state) => state.customer?.contacts);
const categories = useSelector((state) => state.categories?.categories);
const services = useSelector((state) => state.services?.services);
const regions = useSelector((state) => state.regions?.regions);
const stacks = useSelector((state) => state.stacks?.stacks);
const profiles = useSelector((state) => state.profiles?.profiles);
const articles = useSelector((state) => state.articles?.articles);
const category = methods.watch('category');
const service = methods.watch('service');
const region = methods.watch('region');
const customer = methods.watch('customer');
const mergeFormConfigurations = (baseConfig, additionalConfig) => {
return { ...baseConfig, ...additionalConfig };
};
const getFormConfiguration = () => {
let combinedConfigurations = formConfiguration(
category,
customers,
categories,
services,
customerUsers,
regions
);
if (service?.name === 'Consultancy') {
combinedConfigurations = mergeFormConfigurations(
combinedConfigurations,
consultancyFormConfiguration(stacks, profiles)
);
}
if (service?.name === 'New support contract') {
combinedConfigurations = mergeFormConfigurations(
combinedConfigurations,
newSupportContractFormConfiguration(articles)
);
}
if (service?.name === 'Refill support contract') {
combinedConfigurations = mergeFormConfigurations(
combinedConfigurations,
refillSupportContractFormConfiguration(articles, region)
);
}
return combinedConfigurations;
};
const currentFormConfiguration = getFormConfiguration();
const FormWrapper = ({ step, methods }) => {
console.log('FormWrapper');
const configuration = currentFormConfiguration[step];
if (!configuration) {
return null;
}
const onSubmit = (data) => {
const newData = { ...data, articles };
postOffer(newData);
};
const test = methods.watch();
return (
<Box>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NewOffer />
<Save />
<FormComponent
title={configuration.title}
InputFields={configuration.InputFields}
/>
</form>
</Box>
);
};
useEffect(() => {
dispatch(setLocation('generator'));
dispatch(getCategories());
dispatch(getRegions());
}, []);
useEffect(() => {
region && dispatch(getCustomers(1, region));
}, [region]);
useEffect(() => {
category && dispatch(getServices(category?.category_id));
}, [category]);
useEffect(() => {
customer && dispatch(getCustomerContacts(customer.customerId));
}, [customer]);
useEffect(() => {
if (service)
switch (service.name) {
case 'Consultancy': {
dispatch(getStacks());
dispatch(getProfiles());
break;
}
case 'New support contract': {
dispatch(getArticlesByService('support', 'new'));
break;
}
case 'Refill support contract': {
dispatch(getArticlesByService('support', 'refill'));
break;
}
default:
break;
}
}, [service, region]);
console.log('generator');
return (
<Box>
<FormProvider {...methods}>
<StepsBar />
<FormWrapper step={step} methods={methods} />
</FormProvider>
</Box>
);
};
Then a Form configuration exemple :
export const formConfiguration = (
categoryChoice,
customers,
categories,
services,
customerUsers,
regions
) => {
return {
Category: {
title: 'Category',
InputFields: [
{
pieTitle: 'Category',
inputType: 'pie',
register: 'category',
Size: 500,
datas: categories,
},
],
},
Services: {
title: 'Services',
InputFields: [
{
pieTitle: 'Services catalog',
inputType: 'pie',
register: 'service',
Size: 500,
datas: services,
},
],
},
'Informations générales': {
title: 'Informations générales',
InputFields: [
{
inputType: 'radio',
inputTitle: 'Sélectionnez la region concernée',
options: regions,
optionLabel: 'code',
optionValue: 'code',
register: 'region',
},
{
inputType: 'mask',
inputLabel: 'N° DEVIS',
inputTitle: 'Numéro de devis',
mask: 'DEV-99-9999',
register: 'dev',
defaultValue: 'DEV-XX-XXXX',
},
{
inputType: 'text',
inputLabel: 'Référence',
inputTitle: 'Votre référence',
register: 'reference',
type: 'text',
}
],
},
};
};
then I use the fields configuration here
export const FormComponent = ({ title, InputFields }) => {
console.log(InputFields, 'FormComponent');
return (
<Box>
<Typography variant="h4">{title}</Typography>
{InputFields?.map((el) => (
<InputField infos={el} />
))}
</Box>
);
};
The Input switch
//all import
export const InputField = ({ infos }) => {
switch (infos?.inputType) {
case 'radio':
return <Radio infos={infos} />;
case 'text':
return <Text infos={infos} />;
case 'mask':
return <Mask infos={infos} />;
case 'pie':
return <PieChart infos={infos} />;
default:
return null;
}
};
and then a input
export const Text = ({ infos }) => {
const { register } = useFormContext();
return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<FormLabel>{infos?.inputTitle}</FormLabel>
<TextField
label={infos?.inputLabel}
type={infos?.type}
{...register(infos?.register, infos?.registerParams)}
/>
</Box>
);
};
Could using useMemo or useCallback be the right approach to prevent these re-renders, or should I consider restructuring my components? Any suggestions on how to improve the structure or the use of hooks in this scenario would be greatly appreciated.