首页 > 解决方案 > 使用 Formik (React) 更新 Material-UI TextField onBlur

问题描述

我目前正在渲染一个可编辑的表格,让用户可以一次批量编辑多个用户的信息(参见图片)。我正在使用 Material-UI<TextField/>和 Formik 来处理表单的提交和状态。

批量编辑

我试图:

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

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

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

问题似乎在于工作的方式defaultValuevalue属性<TextField />

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

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

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

我也试过:

而且它们似乎都不起作用......在这种情况下我该怎么办?

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


这是我目前正在使用的文本字段:

表单文本

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;

PS:我已经排除了<FormTextArea />组件,因为它与组件完全相同<FormText />

标签: javascriptreactjstypescriptmaterial-uiformik

解决方案


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

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

它看起来像是student.name一个对象,这意味着您key的 s 将是"[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}`}

推荐阅读