如何使用 React Hook Form 和 Material-UI 防止动态表单中的过度重新渲染?

问题描述 投票:0回答:1

我正在使用 React Hook Form 和 Material-UI 开发一个动态表单,其中内容根据前两个用户选择而变化。我的设置包括一个“生成器”页面,该页面根据这些选择呈现不同的表单配置。但是,我遇到了性能问题,因为只要输入值发生变化,整个生成器组件就会重新渲染。

这是我的设置的简化概要:

  • formConfiguration:始终可见且不会改变。
  • combinedConfigurations:根据初始用户选择进行更改。 生成器:根据配置呈现表单。 每个输入修改都会触发生成器的重新渲染,如 console.log('generator') 调用所示。

我尝试使用 useMemo 和 useCallback 来优化性能,但问题仍然存在。

这是我的生成器页面

//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>
  );
};

然后是表单配置示例:

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',
        }
      ],
    },
  };
};

然后我在这里使用 fields 配置

export const FormComponent = ({ title, InputFields }) => {
  console.log(InputFields, 'FormComponent');
  return (
    <Box>
      <Typography variant="h4">{title}</Typography>

      {InputFields?.map((el) => (
        <InputField infos={el} />
      ))}
    </Box>
  );
};

输入开关

//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;
  }
};

然后输入

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>
  );
};

使用 useMemo 或 useCallback 是否是防止这些重新渲染的正确方法,或者我应该考虑重组我的组件?任何有关如何改进结构或在这种情况下使用钩子的建议将不胜感激。

更新:当我删除 FormWrapper 中存在的全局监视时,错误和日志消失。这是我找到的临时解决方案。

听起来全局监视在 FormWrapper 中触发了太多重新渲染。

错误似乎来自 FormWrapper 和 currentFormConfiguration 的步骤

reactjs react-hooks react-redux material-ui react-hook-form
1个回答
0
投票

我通过删除 FormWrapper 并将其逻辑直接集成到 Generator 中来简化结构。这种方法通过避免父组件和子组件之间不必要的状态更新来减少重新渲染的次数。

export const Generator = () => {
  const dispatch = useDispatch();
  const methods = useForm({
    defaultValues: {
      createdBy: 'test',
    },
  });

  const step = useSelector((state) => state.step.actual);
  const articles = useSelector((state) => state.articles?.articles);
  const currentFormConfiguration = methods.watch();

  useEffect(() => {
    dispatch(setLocation('generator'));
    dispatch(getCategories());
    dispatch(getRegions());
  }, []);

  useEffect(() => {
    methods.getValues('region') && dispatch(getCustomers(1, methods.getValues('region')));
  }, [methods.watch('region')]);

  useEffect(() => {
    methods.getValues('category') && dispatch(getServices(methods.getValues('category')?.category_id));
  }, [methods.watch('category')]);

  const onSubmit = (data) => {
    const newData = { ...data, articles };
    postOffer(newData);
  };

  return (
    <Box>
      <FormProvider {...methods}>
        <StepsBar />
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <FormComponent
            title={currentFormConfiguration[step].title}
            InputFields={currentFormConfiguration[step].InputFields}
          />
        </form>
      </FormProvider>
    </Box>
  );
};
© www.soinside.com 2019 - 2024. All rights reserved.