【DailyENJS第7期】掌握 React 函数式组件

DailyENJS 致力于翻译优秀的前端英文技术文章,为技术同窗带来更好的技术视野。前端

多年来,我意识到开发高质量React应用程序的惟一正确方法是编写无状态的函数式组件。react

在本文中,我将简要介绍函数式组件和高阶组件。在此以后,咱们将深刻研究将膨胀的React组件重构为由多个可组合的高阶组件组成的简洁优雅的解决方案。ios

函数式组件介绍

函数组件之因此被称为函数式组件是由于它们就是普通的JavaScript函数。一个优秀的React应用程序应该只包含函数式组件。git

首先让咱们来看一个很是简单的类组件(class component)。github

class MyComponent extends React.Component {
  render() {
    return (
      <div> <h1>Hi, {this.props.name}</h1> </div>
    );
  }
}
复制代码

如今让咱们将其重写为函数式组件:web

const MyComponent = ({name}) => (
  <div> <h1>Hi, {name}</h1> </div>
);
复制代码

如你所见,函数式组件更清晰,更简洁,更易于阅读。也没有必要使用this。axios

其余的一些好处:api

  • 易于理解 - 函数式组件是纯函数,这意味着相同的输入生成相同的输出。给定名称 Ilya ,上面的组件将呈现<h1> Hi,Ilya </ h1>数组

  • 易于测试 - 因为函数式组件是纯函数,所以很容易运行断言:给定一些 props,指望它呈现相应的组件。bash

  • 防止滥用组件的 state

  • 可重复使用的模块化代码

  • 避免出现过于复杂,职责过于庞大的组件

  • 可组合 - 能够根据须要使用更高阶的组件

若是你的组件除了 render 没有其余方法,那么实际上没有什么理由去使用类组件。

高阶组件

高阶组件(HOC)是React中用于重用(和隔离)组件逻辑的技术。你可能已经遇到过HOC——Redux的connect是一个高阶组件。

将HOC应用于组件可以加强现有组件,增长功能。这一般是经过添加新的props来完成的,这些props将传递给你的组件。在Redux connect的例子中,你的组件将得到使用mapStateToProps和mapDispatchToProps函数映射的新props。

咱们常常须要使用localStorage,可是,直接在组件内部与localStorage交互是错误的,由于它是一个反作用。在React中,组件应该没有反作用。如下简单的高阶组件将向包装组件添加三个新props,并使其可以与localStorage交互。

const withLocalStorage = (WrappedComponent) => {
  const loadFromStorage   = (key) => localStorage.getItem(key);
  const saveToStorage     = (key, value) => localStorage.setItem(key, value);
  const removeFromStorage = (key) => localStorage.removeItem(key);

  return (props) => (
      <WrappedComponent loadFromStorage={loadFromStorage} saveToStorage={saveToStorage} removeFromStorage={removeFromStorage} {...props} /> ); } 复制代码

而后咱们能够按照如下方式使用它:

withLocalStorage(MyComponent)
复制代码

凌乱的类组件(A Messy Class Component)

让我向你介绍咱们将要使用的组件。它是一个简单的注册表单,由三个字段和一些基本表单验证组成。

import React from "react";
import { TextField, Button, Grid } from "@material-ui/core";
import axios from 'axios';

class SignupForm extends React.Component {
  state = {
    email: "",
    emailError: "",
    password: "",
    passwordError: "",
    confirmPassword: "",
    confirmPasswordError: ""
  };

  getEmailError = email => {
    const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

    const isValidEmail = emailRegex.test(email);
    return !isValidEmail ? "Invalid email." : "";
  };

  validateEmail = () => {
    const error = this.getEmailError(this.state.email);

    this.setState({ emailError: error });
    return !error;
  };

  getPasswordError = password => {
    const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

    const isValidPassword = passwordRegex.test(password);
    return !isValidPassword
      ? "The password must contain minimum eight characters, at least one letter and one number."
      : "";
  };

  validatePassword = () => {
    const error = this.getPasswordError(this.state.password);

    this.setState({ passwordError: error });
    return !error;
  };

  getConfirmPasswordError = (password, confirmPassword) => {
    const passwordsMatch = password === confirmPassword;

    return !passwordsMatch ? "Passwords don't match." : "";
  };

  validateConfirmPassword = () => {
    const error = this.getConfirmPasswordError(
      this.state.password,
      this.state.confirmPassword
    );

    this.setState({ confirmPasswordError: error });
    return !error;
  };

  onChangeEmail = event =>
    this.setState({
      email: event.target.value
    });

  onChangePassword = event =>
    this.setState({
      password: event.target.value
    });

  onChangeConfirmPassword = event =>
    this.setState({
      confirmPassword: event.target.value
    });

  handleSubmit = () => {
    if (
      !this.validateEmail() ||
      !this.validatePassword() ||
      !this.validateConfirmPassword()
    ) {
      return;
    }

    const data = {
      email: this.state.email,
      password: this.state.password
    };

    axios.post(`https://mywebsite.com/api/signup`, data);
  };

  render() {
    return (
      <Grid container spacing={16}>
        <Grid item xs={4}>
          <TextField
            label="Email"
            value={this.state.email}
            error={!!this.state.emailError}
            helperText={this.state.emailError}
            onChange={this.onChangeEmail}
            margin="normal"
          />

          <TextField
            label="Password"
            value={this.state.password}
            error={!!this.state.passwordError}
            helperText={this.state.passwordError}
            type="password"
            onChange={this.onChangePassword}
            margin="normal"
          />

          <TextField
            label="Confirm Password"
            value={this.state.confirmPassword}
            error={!!this.state.confirmPasswordError}
            helperText={this.state.confirmPasswordError}
            type="password"
            onChange={this.onChangeConfirmPassword}
            margin="normal"
          />

          <Button
            variant="contained"
            color="primary"
            onClick={this.handleSubmit}
            margin="normal"
          >
            Sign Up
          </Button>
        </Grid>
      </Grid>
    );
  }
}

export default SignupForm
复制代码

上面的组件很乱,它一次作不少事情:处理它的state,验证表单字段,以及渲染表单。它已经有140行代码。添加更多功能很快就没法维护。咱们不能作得更好吗?

让咱们看看咱们能作些什么。

须要使用 Recompose

注:Recompose是一个React实用库,用于功能组件和高阶组件。能够把它想象成React的lodash。

Recompose容许你经过添加state,生命周期方法,context等来加强函数式编组件。

最重要的是,它容许你清楚地分离关注点 - 你可让主要组件专门负责布局,负责处理表单输入的高阶组件,另外一个用于处理表单验证,另外一个用于提交表单。它很容易测试!

优雅的函数式组件

让咱们看看能够对复杂的类组件作些什么。

Step 0. 安装 recompose

yarn add recompose
复制代码

Step 1. 提取表单 state

咱们将从Recompose中使用 withStateHandlers。它将容许咱们将组件 state 与组件自己隔离开来。咱们将使用它为电子邮件,密码和确认密码字段添加表单状态,以及上述字段的事件处理程序。

import { withStateHandlers, compose } from "recompose";

const initialState = {
  email: { value: "" },
  password: { value: "" },
  confirmPassword: { value: "" }
};

const onChangeEmail = props => event => ({
  email: {
    value: event.target.value,
    isDirty: true
  }
});

const onChangePassword = props => event => ({
  password: {
    value: event.target.value,
    isDirty: true
  }
});

const onChangeConfirmPassword = props => event => ({
  confirmPassword: {
    value: event.target.value,
    isDirty: true
  }
});

const withTextFieldState = withStateHandlers(initialState, {
  onChangeEmail,
  onChangePassword,
  onChangeConfirmPassword
});

export default withTextFieldState;
复制代码

withStateHandlers 高阶组件很是简单 - 它接受初始state,以及包含state处理程序的对象做为参数。每一个state处理程序在调用时都将返回新的state。

Step 2.提取表单验证逻辑

咱们将从 Recompose 中使用 withProps 高阶组件。它容许将任意 props 添加到现有组件。

咱们将使用 withProps 来添加 emailErrorpasswordErrorconfirmPasswordError props,若是咱们的任何表单字段无效,它们就会出错。

还应该注意,每一个表单字段的验证逻辑都保存在一个单独的文件中(为了更好地分离关注点)。

import { withProps } from "recompose";

const getEmailError = email => {
  if (!email.isDirty) {
    return "";
  }

  const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  const isValidEmail = emailRegex.test(email.value);
  return !isValidEmail ? "Invalid email." : "";
};

const withEmailError = withProps(ownerProps => ({
  emailError: getEmailError(ownerProps.email)
}));

export default withEmailError;
复制代码
import { withProps } from "recompose";

const getPasswordError = password => {
  if (!password.isDirty) {
    return "";
  }

  const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

  const isValidPassword = passwordRegex.test(password.value);
  return !isValidPassword
    ? "The password must contain minimum eight characters, at least one letter and one number."
    : "";
};

const withPasswordError = withProps(ownerProps => ({
  passwordError: getPasswordError(ownerProps.password)
}));

export default withPasswordError;
复制代码
import { withProps } from "recompose";

const getConfirmPasswordError = (password, confirmPassword) => {
  if (!confirmPassword.isDirty) {
      return "";
  }

  const passwordsMatch = password.value === confirmPassword.value;

  return !passwordsMatch ? "Passwords don't match." : "";
};

const withConfirmPasswordError = withProps(
    (ownerProps) => ({
        confirmPasswordError: getConfirmPasswordError(
            ownerProps.password,
            ownerProps.confirmPassword
        )
    })
);

export default withConfirmPasswordError;
复制代码

Step3.提取表单提交逻辑

在这一步中,咱们将提取表单提交逻辑。此次咱们将使用 withHandlers 高阶组件来添加 onSubmit

为何不像之前同样使用 withProps?在 withProps 中使用箭头函数会严重损害性能。withHandlerswithProps 的特殊版本,旨在与箭头函数一块儿使用。

handleSubmit 函数接受从上一步传递下来的 emailErrorpasswordErrorconfirmPasswordError props,检查是否有任何错误,若是没有错误将数据发送到咱们的API。

import { withHandlers } from "recompose";
import axios from "axios";

const handleSubmit = ({
  email,
  password,
  emailError,
  passwordError,
  confirmPasswordError
}) => {
  if (emailError || passwordError || confirmPasswordError) {
    return;
  }

  const data = {
    email: email.value,
    password: password.value
  };

  axios.post(`https://mywebsite.com/api/signup`, data);
};

const withSubmitForm = withHandlers({
  onSubmit: (props) => () => handleSubmit(props)
});

export default withSubmitForm;
复制代码

Step4. 接下来就是见证奇迹的时候

最后,咱们将咱们建立的高阶组件组合到一个能够在咱们的表单上使用的加强器中。咱们将使用 recompose 的 compose 函数,它能够组合多个高阶组件。

import { compose } from "recompose";

import withTextFieldState from "./withTextFieldState";
import withEmailError from "./withEmailError";
import withPasswordError from "./withPasswordError";
import withConfirmPasswordError from "./withConfirmPasswordError";
import withSubmitForm from "./withSubmitForm";

export default compose(
    withTextFieldState,
    withEmailError,
    withPasswordError,
    withConfirmPasswordError,
    withSubmitForm
);
复制代码

注意这个解决方案是多么优雅和干净。全部必需的逻辑只是被添加到另外一个上面以生成一个加强器组件。

Step 5.新的开始

如今让咱们来看看SignupForm组件自己。

mport React from "react";
import { TextField, Button, Grid } from "@material-ui/core";
import withFormLogic from "./logic";

const SignupForm = ({
    email, onChangeEmail, emailError,
    password, onChangePassword, passwordError,
    confirmPassword, onChangeConfirmPassword, confirmPasswordError,
    onSubmit
}) => (
  <Grid container spacing={16}>
    <Grid item xs={4}>
      <TextField
        label="Email"
        value={email.value}
        error={!!emailError}
        helperText={emailError}
        onChange={onChangeEmail}
        margin="normal"
      />

      <TextField
        label="Password"
        value={password.value}
        error={!!passwordError}
        helperText={passwordError}
        type="password"
        onChange={onChangePassword}
        margin="normal"
      />

      <TextField
        label="Confirm Password"
        value={confirmPassword.value}
        error={!!confirmPasswordError}
        helperText={confirmPasswordError}
        type="password"
        onChange={onChangeConfirmPassword}
        margin="normal"
      />

      <Button
        variant="contained"
        color="primary"
        onClick={onSubmit}
        margin="normal"
      >
        Sign Up
      </Button>
    </Grid>
  </Grid>
);

export default withFormLogic(SignupForm);
复制代码

新的重构事后的组件很是干净,只作一件事 - 渲染。单一责任原则规定模块应该作一件事。我相信咱们已经实现了这一目标。

全部必需的数据和输入处理程序都只是做为props传递下来。这反过来使组件很是容易测试。

咱们应该始终努力使咱们的组件彻底不包含逻辑,而且只负责渲染。Recompose 能够帮助咱们作到这一点。

彩蛋:用 pure 优化性能

Recompose 有 pure 组件,这是一个很好的高阶组件,它容许咱们仅在须要时从新渲染组件。pure将确保组件不会从新渲染,除非任何props已更改。

import { compose, pure } from "recompose";

...

export default compose(
  pure,
  withFormLogic
)(SignupForm);
复制代码

总结

咱们应该始终遵循单一职责原则,努力将逻辑与表现隔离开来。咱们首先禁止类组件。主要的组件自己应该是函数式的,而且应该只负责呈现内容而不是其余任何内容。而后将全部必需的状态和逻辑添加为高阶组件。

遵循上述规则将使你的代码清晰,易读,易于维护且易于测试。

原文: medium.com/codeiq/mast…

代码: github.com/suzdalnitsk…

相关文章
相关标签/搜索