【譯】經過Recompose庫掌握React函數組件

聲明:這不是一篇介紹React基礎知識的文章,須要熟悉React相關知識

多年來,我逐漸意識到開發高質量的React應用的惟一正確途徑,是編寫函數組件。react

在本文中,我將簡要介紹函數組件和高階組件。以後,咱們將深刻研究臃腫的React組件,將之重構爲由多個可組合的高階組件的優雅方案。ios

Photo byDean Pughon Unsplashgit

函數組件簡介

之因此被稱爲函數組件,是由於它們確實是普通的JavaScript函數。 經常使用的React中應該只包含函數組件。github

首先讓咱們來看一個很是簡單的Class組件web

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

//simple_class_component.jsx 
複製代碼

如今讓咱們將相同的組件重寫爲函數組件:axios

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

//simple_functional_component.jsx
複製代碼

如您所見,函數組件更清晰,更短,更易於閱讀。也沒有必要使用this關鍵字。api

其餘一些好處:數組

  • 易於推理 - 函數組件是純函數,這意味着它們將始終爲相同的輸入,輸出相同的輸出。 給定名稱Ilya,上面的組件將呈現
<h1> Hi,Ilya </ h1>
複製代碼
  • 易於測試 - 因爲函數組件是純函數,所以很容易對它們進行預測:給定一些props,預測它渲染相應的結構。
  • 幫助防止濫用組件state,採用props替代。
  • 鼓勵可重用和模塊化代碼。
  • 不要讓過於負責的 「god」 components 承擔太多事情
  • 組合性-能夠根據須要使用高階組件添加行爲。

若是你的組件沒有render()方法之外的方法,那麼就沒有理由使用class組件。markdown

高階組件

高階組件(HOC)是React中用於重用(和隔離)組件邏輯的功能。 你可能已經遇到過HOC - Redux的connect是一個高階組件。app

將HOC應用於組件,將用新特性加強現有組件。這一般是經過添加新的props來完成的,這些props會傳遞到組件中。對於Redux的connect,組件將得到新的props,這些props與mapStateToProps和mapDispatchToProps函數進行了映射。

咱們常常須要與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}
        />
  );
}
//simple_hoc.jsx
複製代碼

而後咱們能夠簡單地使用如下方法:withLocalStorage(MyComponent)

凌亂的Class組件

讓我向您介紹咱們將要使用的組件。 它是一個簡單的註冊表單,由三個字段組成,並帶有一些基本的表單驗證。

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;

//complex_form.js
複製代碼

上面的組件很亂,它一次作不少事情:處理它的狀態,驗證表單字段,以及渲染表單。 它已是140行代碼。 添加更多功能很快就沒法維護。 咱們能作得更好嗎?

讓咱們看看咱們能作些什麼。

須要Recompose庫

Recompose是一個React實用庫,用於函數組件和高階組件。把它想象成React的lodash。

Recompose容許你經過添加狀態,生命週期方法,上下文等來加強函數組件。

最重要的是,它容許您清晰地分離關注點 - 你可讓主要組件專門負責佈局,高階組件負責處理表單輸入,另外一個用於處理表單驗證,另外一個用於提交表單。 它很容易測試!

優雅的函數組件

Step 0. 安裝 Recompose

yarn add recompose
複製代碼

Step 1. 提取輸入表單的State

咱們將從Recompose庫中使用withStateHandlers高階組件。 它將容許咱們將組件狀態與組件自己隔離開來。 咱們將使用它爲電子郵件,密碼和確認密碼字段添加表單狀態,以及上述字段的事件處理程序

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;

//withTextFieldState.js
複製代碼

withStateHandlers高階組件很是簡單——它接受初始狀態和包含狀態處理程序的對象。調用時,每一個狀態處理程序將返回新的狀態。

Step 2.提取表單驗證邏輯

如今是時候提取表單驗證邏輯了。咱們將從Recompose中使用withProps高階組件。它容許將任意props添加到現有組件。

咱們將使用withProps添加emailError,passwordError和confirmPasswordError 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;

//withEmailError.js
複製代碼
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;

//withPasswordError.js 
複製代碼
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;

//withConfirmPasswordError.js
複製代碼

Step 3. 提取表單提交邏輯

在這一步中,咱們將提取表單提交邏輯。咱們將再次使用withProps高階組件來添加onSubmit處理程序。

handleSubmit函數接受從上一步傳遞下來的emailError,passwordError和confirmPasswordError props,檢查是否有任何錯誤,若是沒有,則會把參數請求到咱們的API。

import { withProps } 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 = withProps(ownerProps => ({
  onSubmit: handleSubmit(ownerProps)
}));

export default withSubmitForm;

//withSubmitForm.js 
複製代碼

Step 4. 魔術coming

最後,將咱們建立的高階組件組合到一個能夠在咱們的表單上使用的加強器中。 咱們將使用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
);

//withFormLogic.js
複製代碼

請注意此解決方案的優雅和整潔程度。全部必需的邏輯只是簡單地添加到另外一個邏輯上以生成一個加強器組件。

Step 5.呼吸一口新鮮空氣

如今讓咱們來看看SignupForm組件自己。

import 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);

//SignupForm.js 
複製代碼

新的重構組件很是清晰,只作一件事 - 渲染。 單一責任原則規定模塊應該作一件事,它應該作得好。 我相信咱們已經實現了這一目標。

全部必需的數據和輸入處理程序都只是做爲props傳遞下來。 這反過來使組件很是容易測試。

咱們應該始終努力使咱們的組件徹底不包含邏輯,而且只負責渲染。 Recompose容許咱們這樣作。

Project Source Code

若是接下來遇到任何問題,能夠從github下載整個項目

驚喜:使用Recompose的pure能夠優化性能

Recompose有pure,這是一個很好的高階組件,容許咱們只在須要的時候從新呈現組件。pure將確保組件不會從新呈現,除非任何props發生了更改。

import { compose, pure } from "recompose"; 

...

export default compose(
  pure,
  withFormLogic
)(SignupForm);

//pureSignupForm.js
複製代碼

總結:

咱們應該始終遵循組件的單一責任原則,將邏輯與表現隔離開來。爲了實現這一點,首先應該取消Class組件的寫法。主要組件自己應該是功能性的,而且應該只負責呈現而不是其餘任何東西。而後將全部必需的狀態和邏輯添加爲高階組件。

遵循以上規則將使您的代碼清晰明瞭、易於閱讀、易於維護和易於測試。

相關文章
相關標籤/搜索