【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…

相關文章
相關標籤/搜索