我目前正在开发一个涉及大量计算的 React 应用程序。我使用
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 的 debounce 函数来延迟计算,直到用户完成输入值,这反过来又使 TextField 体验超级慢。
同时这是Input组件的代码,在上面的代码中被称为
<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 状态管理库一起使用。
虽然这段代码有效,但我想知道是否有更有效或更简洁的方法来处理这些计算。并管理更清晰的代码。具体来说,我对处理更改和计算总数的方式的替代方法感兴趣。
任何建议或见解将不胜感激。
我的主要建议是将
Formik
替换为 react-hooks-form
。
它比 Formik 快得多,因为它在隔离组件渲染方面做得更好,以避免在单个字段更改时重新渲染整个表单,使用订阅和不受控制的表单(而不是受控表单)。
此外,还有一些针对您的表单的其他反馈:
看起来您在 Formik 中存储了一些不是字段的值(
subTotal
、totalTax
、totalDiscount
、total
)。如果这些值的更改不影响显示的内容,请考虑使用常规状态或引用(因为您将保存一些重新渲染)。
您可能需要使用memoization,以避免在表单中的某些内容发生更改时重新渲染所有组件。
FormikInput
可能是一个不错的起点。