使用 Formik (React) 更新 Material-UI TextField onBlur

Updating a Material-UI TextField onBlur with Formik (React)

我目前正在呈现一个 editable table,它允许用户一次批量编辑多个用户的信息(见图)。我正在使用 Material-UI 的 <TextField/> 和 Formik 来处理表单的提交和状态。

我正在努力:

  1. 保持 <TextField /> 的值和 Formik 的状态同步
  2. 每当我删除一行时(单击 x 时),以反映整个变化 table。

此 table 通常包含大约 266 个输入字段。使用 onChange 事件会带来严重的性能问题。因此,我不得不应用多个组件包装和记忆,以防止每次单个输入更改时重新呈现所有输入字段。

我已经成功地完成了这项工作(几乎以一种良好的性能方式),除了每当我删除一行时。旧值似乎仍然存在,而 Formik 的值确实发生了变化。

问题似乎在于 <TextField />defaultValuevalue 属性如何工作。

value 属性 似乎创建了一个受控组件,并且会一对一地反映您在其中传递的任何值。我试过将 Formik 的 field.value 直接设置到字段中。不幸的是,该值不会更新该字段,因为我目前正在使用 onBlur 事件来执行此操作(并且永远不会显示更改)。如果我要使用 onChange,一切都会正常,除了性能会很差,因为它会更新所有字段。

另一方面,defaultValue 使组件不受控制。尽管如此,我还是可以编辑该值,甚至可以更新 Formik 的状态 onBlur!。 有一个问题,但是...每当我删除一行时,<TextField/> 中的值不会更新(但是Formik 是否反映了变化)。

似乎 <TextField /> 组件内部正在进行一些缓存,因为我已经尝试记录该字段的值,这是我当前传递给 defaultValue 的值,它 正在显示 变化。

我也试过:

其中 none 似乎有效...在这种情况下我该怎么办?

作为参考,这是我使用的代码:


这是我当前使用的文本字段:

FormText

import React, { memo } from 'react';
import { useField } from 'formik';
import TextField from '@material-ui/core/TextField';
import { TextProps } from '../../../Fields/TextField/textfield-definitions';

type ComponentProps = TextProps & {
  useBlur?: boolean;
  errorMessage: string | undefined;
};

export const Component: React.FC<ComponentProps> = memo(props => {
  const {
    className,
    name,
    label,
    placeholder,
    required,
    useBlur,
    error,
    errorMessage,
    onChange,
    onBlur,
    value,
  } = props;

  // We wrap it so we don't block the heap stack!
  // Improves performance considerably
  // https://medium.com/trabe/react-syntheticevent-reuse-889cd52981b6
  const fireBlur = (e: any) => {
    // React removes
    e.persist();
    window.setTimeout(() => {
      if (onBlur) {
        onBlur(e);
      }
    }, 0);
  };

  const setInnerState = (e: React.ChangeEvent<HTMLInputElement>) => {};

  const fireChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    e.persist();
    setInnerState(e);
    window.setTimeout(() => {
      if (onChange) {
        onChange(e);
      }
    }, 0);
  };

  return (
    <TextField
      className={className}
      name={name}
      label={label}
      type={props.type}
      placeholder={placeholder}
      defaultValue={value}
      variant="outlined"
      required={required}
      error={error}
      helperText={<span>{error ? errorMessage : ''}</span>}
      onChange={useBlur ? undefined : fireChange}
      onBlur={useBlur ? fireBlur : undefined}
    />
  );
});

export const SchonText: React.FC<TextProps> = props => {
  const [field, meta] = useField(props.name);
  const hasError = !!meta.error && !!meta.touched;
  return (
    <Component
      value={field.value}
      {...props}
      error={hasError}
      errorMessage={meta.error}
      onChange={field.onChange}
      onBlur={field.onChange}
    />
  );
};

export default SchonText;

以下是使用它的组件:
TableRow

import React, { memo } from 'react';
import { TableRow, TableCell, makeStyles } from '@material-ui/core';
import { Close } from '@material-ui/icons';
import {
  FormText,
  FormSelect,
  FormTextArea,
  Button,
} from '../../../../../../components';
import { Student, Gender } from '../../../../../../graphql/types';
import { SelectOption } from '../../../../../../components/Fields/Select/select-definitions';

type BulkAddTableRowProps = {
  student: Student;
  index: number;
  deleteStudent: (index: number) => void;
};
const useStyles = makeStyles(theme => ({
  root: {
    padding: `0px`,
  },
}));

const selectOptions: SelectOption[] = [
  {
    label: 'M',
    value: Gender.Male,
  },
  {
    label: 'F',
    value: Gender.Female,
  },
];

const Component: React.FC<BulkAddTableRowProps> = props => {
  const styles = useStyles();
  const { student, index } = props;
  const deleteStudent = () => props.deleteStudent(index);
  return (
    <TableRow className={styles.root} hover={true}>
      <TableCell>{index + 1}</TableCell>
      <TableCell className={styles.root}>
        <FormText
          name={`students[${index}].name.firstName`}
          value={student.name.firstName}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <FormText
          name={`students[${index}].name.lastName`}
          value={student.name.lastName}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <FormSelect
          name={`students[${index}].gender`}
          value={student.gender}
          options={selectOptions}
        />
      </TableCell>
      <TableCell>
        <FormText
          type="email"
          name={`students[${index}].email`}
          value={student.email}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <FormText
          type="date"
          name={`students[${index}].birthDate`}
          value={student.birthDate}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <FormTextArea
          name={`students[${index}].allergies`}
          value={student.allergies}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <FormTextArea
          name={`students[${index}].diseases`}
          value={student.diseases}
          useBlur={true}
        />
      </TableCell>
      <TableCell>
        <Button onClick={deleteStudent}>
          <Close />
        </Button>
      </TableCell>
    </TableRow>
  );
};

function shouldRemainTheSame(
  prevProps: BulkAddTableRowProps,
  newProps: BulkAddTableRowProps,
): boolean {
  const prevStudent = prevProps.student;
  const newStudent = newProps.student;
  const isNameTheSame = Object.keys(prevStudent.name).every(key => {
    return prevStudent.name[key] === newStudent.name[key];
  });
  const isStudentTheSame = Object.keys(prevStudent)
    .filter(x => x !== 'name')
    .every(key => prevStudent[key] === newStudent[key]);
  return (
    isNameTheSame && isStudentTheSame && prevProps.index === newProps.index
  );
}

export const BulkAddTableRow = memo(Component, shouldRemainTheSame);
export default BulkAddTableRow;

StudentBulkTableView

import React, { memo } from 'react';
import {
  FieldArray,
  FieldArrayRenderProps,
  getIn,
  useFormikContext,
} from 'formik';
import { Student, Gender } from '../../../../graphql/types/index';
import {
  Paper,
  Table,
  TableHead,
  TableRow,
  TableCell,
  TableBody,
  makeStyles,
} from '@material-ui/core';
import { Button, Select } from '../../../../components';
import { SelectOption } from '../../../../components/Fields/Select/select-definitions';
import { emptyStudent, BulkAddStudentValues } from '../shared';
import BulkAddTableRow from './components/TableRow/index';

type ComponentProps = {
  push: (obj: any) => void;
  remove: (index: number) => undefined;
  students: Student[];
  setFieldValue: (
    field: 'students',
    value: any,
    shouldValidate?: boolean | undefined,
  ) => void;
};

const selectOptions: SelectOption[] = [
  {
    label: 'M',
    value: Gender.Male,
  },
  {
    label: 'F',
    value: Gender.Female,
  },
];

const useStyles = makeStyles(theme => ({
  root: {
    padding: `0px`,
  },
}));

const Component: React.FC<ComponentProps> = memo(props => {
  const styles = useStyles();
  const { students, push, remove, setFieldValue } = props;
  function deleteStudent(index: number) {
    if (!window.confirm('¿Desea borrar este estudiante?')) {
      return;
    }
    remove(index);
  }

  const addStudent = () => push(emptyStudent());

  const selectAllOptions = (evt: React.ChangeEvent<HTMLInputElement>) => {
    students.forEach(student => (student.gender = evt.target.value as Gender));
    console.log(students);
    setFieldValue('students', students);
  };

  return (
    <>
      Cambiar el género a todos los estudiantes:{' '}
      <Select
        name="select_all"
        options={selectOptions}
        onChange={selectAllOptions}
      />{' '}
      <br />
      <Paper style={{ width: '100%' }}>
        <Table style={{ width: '100%', padding: 'root' }}>
          <TableHead>
            <TableRow>
              <TableCell>#</TableCell>
              <TableCell>Nombre</TableCell>
              <TableCell>Apellido</TableCell>
              <TableCell>Género</TableCell>
              <TableCell>Email</TableCell>
              <TableCell>Cumpleaños</TableCell>
              <TableCell>Alergias</TableCell>
              <TableCell>Enfermedades</TableCell>
              <TableCell>Acción</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {students.map((student, index) => (
              <BulkAddTableRow
                key={`${student.name}-${index}`}
                student={student}
                deleteStudent={deleteStudent}
                index={index}
              />
            ))}
            <TableRow>
              <TableCell colSpan={8}></TableCell>
              <TableCell>
                <Button onClick={addStudent}>+</Button>
              </TableCell>
            </TableRow>
          </TableBody>
        </Table>
      </Paper>
    </>
  );
});

export const StudentBulkTableView: React.FC = props => {
  const { setFieldValue } = useFormikContext<BulkAddStudentValues>();

  return (
    <FieldArray name="students">
      {({ remove, push, form }: FieldArrayRenderProps) => {
        const students = getIn(form.values, 'students') as Student[];

        return (
          <Component
            setFieldValue={setFieldValue}
            remove={remove}
            push={push}
            students={students}
          />
        );
      }}
    </FieldArray>
  );
};
export default StudentBulkTableView;

P.S:我排除了 <FormTextArea /> 组件,因为它与 <FormText /> 组件完全相同。

根据您描述的行为,听起来您对每一行使用的 key 可能有问题。

<BulkAddTableRow
  key={`${student.name}-${index}`}

看起来 student.name 是一个对象,这意味着您的 key 将是 "[object Object]-0""[object Object]-1" 等。基于索引的键在以下情况下会导致问题删除行,因为 React 不知道该索引的值已更改。

这是一篇描述问题的文章:https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318

你可以console.log每一行的key,如果它们是[object-Object]加上索引,你可以这样做:

<BulkAddTableRow
  key={`${student.name.firstName}-${student.name.lastName}`}

这个答案适用于@PrateekPareek,我将分享如何使用 Formik 2 实现高性能的 onBlur。

这将使用 Hooks,这与我上面发布的有很大不同。

我也在用 Material UI.

这里的技巧是使用内部状态来处理 Material UI TextField 并使用 onBlur 将更改传播到 Formik。不幸的是,这有一个缺点,即无法检测到密码等预填字段。

为了缓解这种情况,有一种替代方法使用去抖动函数和 useEffect 在用户键入时传播 onChange 事件,而不是使用 onBlur(我没有在这里分享,因为我还没有创建它).

我通常倾向于将我的表单文本字段包装在它们自己的组件中,所以只需将它们放入正在使用的组件中即可。

有一个“disablePerformance”道具,基本上可以让 Formik 处理整个组件。

import React, { memo, useState } from 'react';
import {
  TextField,
  TextFieldProps,
} from '../../../../../stories/Fields/TextField';
import { useField } from 'formik';

export type FormTextProps = Omit<TextFieldProps, 'name'> & {
  name: string;
  disablePerformance?: boolean;
};

export const FormText: React.FC<FormTextProps> = memo((props) => {
  const [field, meta] = useField(props.name);
  const error = !!meta.error && meta.touched;

  /**
   * For performance reasons (possible due to CSS in JS issues), heavy views
   * affect re-renders (Formik changes state in every re-render), bringing keyboard
   * input to its knees. To control this, we create a setState that handles the field's inner
   * (otherwise you wouldn't be able to type) and then propagate the change to Formik onBlur and
   * onFocus.
   */
  const [fieldValue, setFieldValue] = useState<string | number>(field.value);
  const { disablePerformance, ...otherProps } = props;
  const onChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
    setFieldValue(evt.target.value);
  };
  const onBlur = (evt: React.FocusEvent<HTMLInputElement>) => {
    field.onChange({
      target: {
        name: props.name,
        value: evt.target.value || '',
      },
    });
  };

  // Will set depending on the performance props
  const performanceProps = disablePerformance
    ? field
    : {
        ...field,
        value: fieldValue,
        onChange,
        onBlur,
        onFocus: onBlur,
      };

  return (
    <>
      <TextField
        {...otherProps}
        error={error}
        helperText={meta.touched && meta.error}
        {...performanceProps}
      />
    </>
  );
});

export default FormText;

仅供参考,这是 TextField,它是 Material-UI 的包装器,因此我可以设置一些默认值。

import React, { memo } from 'react';
import {
  TextField as MuiTextField,
  TextFieldProps as MuiTextFieldProps,
} from '@material-ui/core';

export type TextFieldProps = MuiTextFieldProps & {};

export const TextField: React.FC<TextFieldProps> = memo((props) => {
  // muiProps => The props for MaterialUI
  const { ...muiProps } = props;
  return (
    <>
      <MuiTextField
        fullWidth={true}
        InputLabelProps={{
          shrink: true,
          style: { textTransform: 'uppercase' },
        }}
        {...muiProps}
      >
        {props.children}
      </MuiTextField>
    </>
  );
});

export default TextField;