重构几乎相同的组件,formik react

Refactoring almost identical components, formik react

我有两个相同的组件,只有很少的差异(一个)。有两个很多重复的代码和样板,但我不确定如何重构它,所以我可能只需要提供一个配置。

LoginPage.js

import React from 'react';
import { Link } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';

import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import { LoginValidationSchema } from 'validations/AuthValidationSchema';

function LoginPage({ username, onChangeUsername, onSubmitForm }) {
  return (
    <div>
      <Helmet>
        <title>Login</title>
      </Helmet>
      <Formik
        initialValues={{ username, password: '' }}
        validationSchema={LoginValidationSchema}
        onSubmit={onSubmitForm}
        render={({ isSubmitting, isValid, handleChange }) => (
          <Form>
            <FastField
              type="text"
              name="username"
              render={({ field }) => (
                <input
                  {...field}
                  onChange={e => {
                    handleChange(e);
                    onChangeUsername(e);
                  }}
                />
              )}
            />
            <ErrorMessage name="username" component="div" aria-live="polite" />
            <FastField type="password" name="password" />
            <ErrorMessage name="password" component="div" aria-live="polite" />
            <button type="submit" disabled={isSubmitting || !isValid}>
              Login
            </button>
            <FormDebug />
          </Form>
        )}
      />
      <Link to="/auth/forgot_password">Forgot Password</Link>
    </div>
  );
}

LoginPage.propTypes = {
  username: PropTypes.string,
  onSubmitForm: PropTypes.func.isRequired,
  onChangeUsername: PropTypes.func.isRequired,
};

export default LoginPage;

ForgotPasswordPage.js

import React from 'react';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';

import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import { ForgotPasswordValidationSchema } from 'validations/AuthValidationSchema';

function ForgotPasswordPage({ username, onChangeUsername, onSubmitForm }) {
  return (
    <div>
      <Helmet>
        <title>Forgot Password</title>
      </Helmet>
      <Formik
        initialValues={{ username }}
        validationSchema={ForgotPasswordValidationSchema}
        onSubmit={onSubmitForm}
        render={({ isSubmitting, isValid, handleChange }) => (
          <Form>
            <FastField
              type="text"
              name="username"
              render={({ field }) => (
                <input
                  {...field}
                  onChange={e => {
                    handleChange(e);
                    onChangeUsername(e);
                  }}
                />
              )}
            />
            <ErrorMessage name="username" component="div" aria-live="polite" />
            <FormDebug />
            <button type="submit" disabled={isSubmitting || !isValid}>
              Reset Password
            </button>
          </Form>
        )}
      />
    </div>
  );
}

ForgotPasswordPage.propTypes = {
  username: PropTypes.string,
  onSubmitForm: PropTypes.func.isRequired,
  onChangeUsername: PropTypes.func.isRequired,
};

export default ForgotPasswordPage;

如果你是我,你将如何重构它。

我正在考虑 HOC,但我不确定如何调用不同的 "children" 传递

我可能遗漏了此处未说明的一些复杂性,但这看起来就像创建一个接受更多传入属性的通用功能组件一样简单。我做了一个 diff,你只需要添加 'title'、'buttonText',当然,如果你愿意,还可以添加 'type'。您还可以将 initialValues 对象作为道具发送,而不是从 'type' 派生它。

我的意思是,您尝试过以下方法吗?

import React from 'react';
import { Link } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Formik, FastField, Form, ErrorMessage } from 'formik';

import PropTypes from 'prop-types';
import { FormDebug } from 'utils/FormDebug';
import * as schema from 'validations/AuthValidationSchema';

function AuthPage({ buttonText, initialValues, title, type, username,
                    onChangeUsername, onSubmitForm }) {
  const authSchema = type === 'login' 
        ? schema.LoginValidationSchema 
        : schema.ForgotPasswordValidationSchema;

  return (
    <div>
      <Helmet>
        <title>{title}</title>
      </Helmet>
      <Formik
        initialValues={initialValues}
        validationSchema={authSchema}
        onSubmit={onSubmitForm}
        render={({ isSubmitting, isValid, handleChange }) => (
          <Form>
            <FastField
              type="text"
              name="username"
              render={({ field }) => (
                <input
                  {...field}
                  onChange={e => {
                    handleChange(e);
                    onChangeUsername(e);
                  }}
                />
              )}
            />
            <ErrorMessage name="username" component="div" aria-live="polite" />
            {type === 'forgot' &&
              <FastField type="password" name="password" />
              <ErrorMessage name="password" component="div" aria-live="polite" />
            }
            <button type="submit" disabled={isSubmitting || !isValid}>
              {buttonText}
            </button>
            <FormDebug />
          </Form>
        )}
      />
      <Link to="/auth/forgot_password">Forgot Password</Link>
    </div>
  );
}

AuthPage.propTypes = {
  buttonText: PropTypes.string,
  initialValues: PropTypes.object,
  title: PropTypes.string,
  type: PropTypes.oneOf(['login', 'forgot'])
  username: PropTypes.string,
  onSubmitForm: PropTypes.func.isRequired,
  onChangeUsername: PropTypes.func.isRequired,
};

export default AuthPage;

(我唯一不记得的是密码字段的条件渲染及其错误消息是否需要包装在 div 中或不使它们成为一个元素)

如果不想传入初始值:

  const initialVals = type === 'login'
        ? { username, password: ''}
        : { username }
  ...
  initialValues={initialVals}

并将其从 propTypes 中删除

我唯一不确定的是为什么 FormDebug 在两个版本中的位置不同。我把它留在 按钮之后。

抱歉,如果您不是在寻找一般性答案,但我认为您不会通过概括看似相关的组件来提高可维护性。我预计这些组件会随着您的应用程序的成熟而进一步分离,例如通过添加社交登录、'remember me' 选项、验证码、通过电子邮件检索用户名和密码的选项、检索密码与登录时对未知用户名的不同处理等。此外,这是您组件的一部分真的不想出错,所以 KISS 和所有。最后,考虑一下这样的 semi-generalised login-or-retrieve-password 表单组件是否真的有第三个用例。

不过,可以通过例如创建一个可重用的 UsernameField 组件,该组件的使用对于这两种情况都很简单且一致。还可以考虑使用 withValidation HOC 向字段添加错误消息。如果你真的想扩展它,你可以为 Formik 使用 withSubmit HOC,将所有道具传递给 Formik,呈现 children (你将传递 handleChange 道具)和提交按钮。我假设表单本身使用上下文将状态传递给 ErrorMessage 和 FastField。

这两个页面有不同的关注点,所以我不建议提取可用于这两种情况的逻辑,因为即使它确实删除了一些重复的行,也会通过将代码进一步分开来增加复杂性。在这种情况下,一个好的策略可能是考虑在组件中重用的东西。

例如,您可以将 formik 组件包装在您自己的包装器中,然后再包装一个适用于您的新表单的输入组件。减少样板的一个很好的练习可能是针对此最终结果的某些变体

<CoolForm 
  initialValues={{ username, password: '' }}
  validationSchema={LoginValidationSchema}
>
  <CoolFormInputWithError someProps={{howeverYou: 'want to design this'}}>
  <CoolFormInputWithError someProps={{howeverYou: 'want to design this'}}>
  <CoolFormSubmit>
    Login
  </CoolFormSubmit>
  <FormDebug />
</CoolForm>

这只是一个想法,但这种策略的好处在于,如果你出于任何原因想要重构 formik,它真的很简单,因为你所有的代码现在都包装在 CoolForm 组件中,所以你最终只需要改变您自己的组件的实现,尽管在这种情况下好处很小。