Formik 状态变化

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

我目前正在开发一个涉及大量计算的 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 状态管理库一起使用。

虽然这段代码有效,但我想知道是否有更有效或更简洁的方法来处理这些计算。并管理更清晰的代码。具体来说,我对处理更改和计算总数的方式的替代方法感兴趣。

任何建议或见解将不胜感激。

javascript reactjs typescript formik jotai
1个回答
0
投票

我的主要建议是将

Formik
替换为
react-hooks-form

它比 Formik 快得多,因为它在隔离组件渲染方面做得更好,以避免在单个字段更改时重新渲染整个表单,使用订阅和不受控制的表单(而不是受控表单)。

此外,还有一些针对您的表单的其他反馈:

  • 看起来您在 Formik 中存储了一些不是字段的值(

    subTotal
    totalTax
    totalDiscount
    total
    )。如果这些值的更改不影响显示的内容,请考虑使用常规状态或引用(因为您将保存一些重新渲染)。

  • 您可能需要使用memoization,以避免在表单中的某些内容发生更改时重新渲染所有组件。

    FormikInput
    可能是一个不错的起点。

© www.soinside.com 2019 - 2024. All rights reserved.