我目前正在开发一个涉及大量计算的Reaction应用程序.我使用Formik来管理表单,使用lodash来执行一些实用功能.以下是我的代码片段:

import { useEffect, useRef } from 'react';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
import FormikControl from './FormikControl';
import { Field, ErrorMessage, FieldArray } from 'formik';
import DeleteIcon from '@material-ui/icons/Delete';
import { Box, Button, Grid } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import productData from '../../Data/products.json';
import { INITIAL_VALUES } from 'src/utils/utils';
import { useFormikContext } from 'formik';
import _ from 'lodash';
import { convertNumberToWords } from 'src/services/services';

interface Column {
  id: 'Product' | 'Quantity' | 'Unit Price' | 'Amount' | 'Action';
  label: string;
  minWidth?: number;
  align?: any;
  format?: (value: number) => string;
}

const columns: readonly Column[] = [
  { id: 'Product', label: 'Product', minWidth: 170 },
  { id: 'Quantity', label: 'Quantity', minWidth: 100, align: 'center' },
  {
    id: 'Unit Price',
    label: 'Unit Price',
    minWidth: 170,
    align: 'center',
    format: (value: number) => value.toLocaleString('en-US')
  },
  {
    id: 'Amount',
    label: 'Amount',
    minWidth: 100,
    align: 'center',
    format: (value: number) => value.toLocaleString('en-US')
  },
  {
    id: 'Action',
    label: 'Action',
    minWidth: 100,
    align: 'center',
    format: (value: number) => value.toFixed(2)
  }
];

const generateId = () => {
  return Date.now().toString(36) + Math.random().toString(36).substring(2);
};

const FormikTable = (props) => {
  const { label, name, values, ...rest } = props;
  const handleUnitsChange = (index, units) => {
    const unitPrice = Number(values[index].unitPrice);
    const unitTotal = parseFloat((units * unitPrice).toFixed(2));
    formik.setFieldValue(`${name}.${index}.units`, units);
    formik.setFieldValue(`${name}.${index}.unitTotal`, unitTotal);
  };

  const handleProductChange = (index, product) => {
    formik.setFieldValue(`${name}.${index}.name`, product);
    formik.setFieldValue(`${name}.${index}.unitPrice`, product.price);
  };

  const handleUnitPriceChange = (index, unitPrice) => {
    const units = Number(values[index].units);
    const unitTotal = parseFloat((units * unitPrice).toFixed(2));
    formik.setFieldValue(`${name}.${index}.unitPrice`, unitPrice);
    formik.setFieldValue(`${name}.${index}.unitTotal`, unitTotal);
  };

  const handleUnitTotalChange = (index, unitTotal) => {
    const units = Number(values[index].units);
    const unitPrice =
      units !== 0 ? parseFloat((unitTotal / units).toFixed(2)) : 0;
    formik.setFieldValue(`${name}.${index}.unitTotal`, unitTotal);
    formik.setFieldValue(`${name}.${index}.unitPrice`, unitPrice);
  };

  const formik = useFormikContext();

  const calculateTotal = (products) => {
    return products.reduce(
      (total, product) => total + Number(product.unitTotal),
      0
    );
  };

  const calculateSubTotal = (products) => {
    return products.reduce(
      (total, product) =>
        total + Number(product.unitPrice) * Number(product.units),
      0
    );
  };

  const debouncedSave = useRef(
    _.debounce((values) => {
      values.forEach((product, index) => {
        const unitTotal = Number(product.unitPrice) * Number(product.units);
        formik.setFieldValue(`${name}.${index}.unitTotal`, unitTotal);
      });
    }, 100)
  ).current;

  useEffect(() => {
    const subtotal = parseFloat(calculateSubTotal(values).toFixed(2));
    formik.setFieldValue('subTotal', Number(subtotal));

    const taxRate = formik.values.taxRate;
    const tax = parseFloat(((taxRate / 100) * subtotal).toFixed(2));
    formik.setFieldValue('totalTax', Number(tax));

    const discountRate = formik.values.discountRate;
    const discount = parseFloat(((discountRate / 100) * subtotal).toFixed(2));
    formik.setFieldValue('totalDiscount', Number(discount));

    const total = parseFloat((subtotal + tax - discount).toFixed(2));
    formik.setFieldValue('total', Number(total));

    debouncedSave(values);
  }, [values, formik.values.taxRate, formik.values.discountRate]);

  convertNumberToWords(123);

  return (
    <FieldArray name={name}>
      {({ insert, remove, push, setFieldValue }) => {
        return (
          <>
            {
              <TableContainer sx={{ maxHeight: 440 }}>
                <Table stickyHeader aria-label="sticky table">
                  <TableHead>
                    <TableRow>
                      {columns.map((column) => (
                        <TableCell
                          key={column.id}
                          align={column.align}
                          style={{ minWidth: column.minWidth }}
                        >
                          {column.label}
                        </TableCell>
                      ))}
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {values.map((product, index) => (
                      <TableRow key={product.id}>
                        <TableCell>
                          <FormikControl
                            control="autocomplete"
                            type="text"
                            name={`${name}.${index}.name`}
                            options={productData}
                            getOptionLabel={(option: any) => option?.name}
                            onChange={(e, product) =>
                              handleProductChange(index, product)
                            }
                          />
                        </TableCell>
                        <TableCell>
                          <FormikControl
                            control="input"
                            type="number"
                            name={`${name}.${index}.units`}
                            onChange={(e) =>
                              handleUnitsChange(index, e.target.value)
                            }
                          />
                        </TableCell>
                        <TableCell>
                          <FormikControl
                            control="input"
                            type="number"
                            name={`${name}.${index}.unitPrice`}
                            defaultValue={values[index].name?.price}
                            onChange={(e) =>
                              handleUnitPriceChange(index, e.target.value)
                            }
                          />
                        </TableCell>
                        <TableCell>
                          <FormikControl
                            control="input"
                            type="number"
                            name={`${name}.${index}.unitTotal`}
                            onChange={(e) =>
                              handleUnitTotalChange(index, e.target.value)
                            }
                          />
                        </TableCell>
                        <TableCell>
                          <Button onClick={() => remove(index)}>
                            <DeleteIcon />
                          </Button>
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                  <Button
                    onClick={() =>
                      push({
                        id: generateId(),
                        name: {},
                        units: 0,
                        unitPrice: 0,
                        unitVat: 0,
                        unitTotal: 0
                      })
                    }
                  >
                    Add Row
                  </Button>
                </Table>
              </TableContainer>
            }
          </>
        );
      }}
    </FieldArray>
  );
};

export default FormikTable;

在这段代码中,我处理对单位、产品、单价和单位合计的更改.我还在计算小计、税、折扣和合计.我使用loDash的go 反跳函数来延迟计算,直到用户完成输入值,而这反过来又会使Textfield体验变得非常慢.

同时,这里是输入组件的代码,在上面的代码中称为<FormikControl control="input" .... />


import React from 'react';
import { Field, ErrorMessage } from 'formik';
import { TextField, InputLabel } from '@mui/material';

const FormikInput = (props) => {
  const { label, defaultValue, name, ...rest } = props;
  return (
    <Field name={name}>
      {({ field, form }) => {
        return (
          <>
            <InputLabel
              style={{ color: ' #5A5A5A', marginBottom: '5px' }}
              htmlFor={name}
            >
              {label}
            </InputLabel>

            <TextField
            fullWidth
              id={name}
              {...field}
              {...rest}
              value={defaultValue}
              error={form.errors[name] && form.touched[name]}
              helperText={<ErrorMessage name={name} />}
              // InputProps={{
              //   style: { height: '40px', borderRadius: '5px' },
              // }}
            />
          </>
        );
      }}
    </Field>
  );
};

export default FormikInput;

我正在使用Jotai作为我的国家管理库,但我不能理解如何将Formik与Jotai国家管理库一起使用.

虽然这段代码可以工作,但我想知道是否有更有效或更干净的方法来处理这些计算.并管理更干净的代码.具体地说,我对处理更改和计算总数的方式以外的其他方法感兴趣.

如有任何建议或见解,我们将不胜感激.

推荐答案

我的主要建议是用react-hook-form取代Formik.

它比Formik高much faster倍,因为它在隔离组件呈现方面做得更好,从而避免在单个字段更改时使用订阅和非受控表单(而不是受控表单)重新呈现整个表单.

此外,还有一些特定于您的表单的其他反馈:

  • 看起来您在Formik中存储的一些值不是字段(subTotaltotalTaxtotalDiscounttotal).如果更改这些值不会影响显示的内容(因为您将节省一些重新渲染),请考虑使用常规状态或引用.

  • 您可能希望使用memoization,以避免在表单中发生更改时重新呈现所有组件.FormikInput可能是一个很好的起点.

  • 避免使用所有的.toFixed()Number()转换.您可以简单地使用number,并且在渲染这些值时仅使用.toFixed().

Edit:

关于你在 comments 中问到的如何/在哪里计算subTotaltotalTaxtotalDiscounttotal,假设你想在表格之后显示它们,你有几个 Select :

一种 Select 是在每次渲染时简单地计算它们(可能使用useMemo):

const {
  subtotal,
  totalTax,
  totalDiscount,
  total,
} = (() => {
  const subTotal = calculateSubTotal(values);
  const totalTax = (taxRate / 100) * subtotal;
  const totalDiscount = (discountRate / 100) * subtotal;
  const total = subtotal + tax - discount;

  return { subTotal, totalTax, totalDiscount, total }
}, [values, taxRate, discountRate]);

return (
  <>
    <Form>
       ...
    </Form>

    <div>{ subTotal }</div>
    <div>{ totalTax }</div>
    <div>{ totalDiscount }</div>
    <div>{ total }</div>
  </>    
)

如果您从使用react-hook-form中获得的性能改进足够并且产品列表不太长,那么这可能是很好的.

或者,您可以使用React的18、useTransition()useDeferredValue(),以便Reaction决定何时重新计算和重新呈现这些值,而不会使用户界面感觉没有响应.

它们与您使用的debounce类似,但这种方式会根据其他剩余的工作负载决定何时或何时不计算这些更新(重新呈现列表/表单将比更新总计更优先)以及执行此操作所需的时间(如果列表很短,您可能不会注意到延迟,而debouce总是会带来相同的延迟).

下面是一个useTransition()分的例子:

const [isPending, startTransition] = useTransition();

const [{
  subtotal,
  totalTax,
  totalDiscount,
  total,
}, setResult] = useState({
  subtotal: 0,
  totalTax: 0,
  totalDiscount: 0,
  total: 0,
});

useEffect(() => {
  startTransition(() => {
    const subTotal = calculateSubTotal(values);
    const totalTax = (taxRate / 100) * subtotal;
    const totalDiscount = (discountRate / 100) * subtotal;
    const total = subtotal + tax - discount;

    setResult({ subTotal, totalTax, totalDiscount, total })
  });
}, [values, taxRate, discountRate]);

return (
  <>
    <Form>
       ...
    </Form>

    <div>{ subTotal }</div>
    <div>{ totalTax }</div>
    <div>{ totalDiscount }</div>
    <div>{ total }</div>
  </>    
)

下面是一个useDeferredValue()人的例子:

return (
  <>
    <Form>
       ...
    </Form>

    <CartTotal
      values ={ values }
      taxRate={ taxRate }
      discountRate={ discountRate } />
  </>    
)

const CartTotal = ({
  values: valuesProp,
  taxRate: taxRateProp,
  discountRate: discountRateProp,
}) => {
  const values = useDeferredValue(valuesProp);
  const taxRate = useDeferredValue(taxRateProp);
  const discountRate = useDeferredValue(discountRateProp);

  const {
    subtotal,
    totalTax,
    totalDiscount,
    total,
  } = (() => {
    const subTotal = calculateSubTotal(values);
    const totalTax = (taxRate / 100) * subtotal;
    const totalDiscount = (discountRate / 100) * subtotal;
    const total = subtotal + tax - discount;

    return { subTotal, totalTax, totalDiscount, total }
  }, [values, taxRate, discountRate]);

  return (
    <div>
      <div>{ subTotal }</div>
      <div>{ totalTax }</div>
      <div>{ totalDiscount }</div>
      <div>{ total }</div>
    </div>    
  )
}

您也可以try 在Formik的原始代码中使用这些方法,但我认为您的问题主要是因为不断地重新呈现整个FieldArray(由于Formik的内部工作方式),所以我不指望这本身会对事情有太大的改善.

Javascript相关问答推荐

为什么子组件没有在reaction中渲染?

如何在alpinejs中显示dev中x-for的元素

当promise 在拒绝处理程序被锁定之前被拒绝时,为什么我们会得到未捕获的错误?

Angular material 拖放堆叠的牌副,悬停时自动展开&

构造HTML表单以使用表单数据创建对象数组

如何调用名称在字符串中的实例方法?

获取Uint8ClampedArray中像素数组的宽度/高度

如何在coCos2d-x中更正此错误

如何在每次单击按钮时重新加载HighChart/设置HighChart动画?

覆盖加载器页面避免对页面上的元素进行操作

VSCode中出现随机行

当我在Reaction中创建一个输入列表时,我的输入行为异常

更新Redux存储中的对象数组

为什么延迟在我的laravel项目中不起作用?

如何在一个对象Java脚本中获取不同键的重复值?

如何组合Multer上传?

使用CEPRESS截取时,cy.Wait()在等待5000ms的第一个路由请求时超时

为什么当雪碧的S在另一个函数中时,Phaser不能加载它?

对不同目录中的Angular material 表列进行排序

使用props 将VUE 3组件导入JS文件