声明:这不是一篇介绍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
其余一些好处:数组
<h1> Hi,Ilya </ h1>
复制代码
若是你的组件没有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)
让我向您介绍咱们将要使用的组件。 它是一个简单的注册表单,由三个字段组成,并带有一些基本的表单验证。
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组件的写法。主要组件自己应该是功能性的,而且应该只负责呈现而不是其余任何东西。而后将全部必需的状态和逻辑添加为高阶组件。
遵循以上规则将使您的代码清晰明了、易于阅读、易于维护和易于测试。