这是高阶组件(HOC)系列的第二部分。 今天,我将介绍有用且可实现的不同的高阶组件模式。 使用HOC,您可以将冗余代码抽象为更高层次的层。 但是,像其他模式一样,要习惯HOC也需要一些时间。 本教程将帮助您弥合这一差距。
先决条件
我建议您按照本系列的第一部分进行操作。 在第一部分中,我们讨论了HOC语法基础知识以及开始使用高阶组件所需的一切。
在本教程中,我们将在第一部分已经介绍的概念的基础上进行构建。 我创建了几个实用的示例HOC,您可以将这些想法合并到您的项目中。 每个部分都提供了代码片段,并且在本教程的结尾处提供了本教程中讨论的所有实用HOC的工作演示。
您也可以从我的GitHub存储库中派生代码。
实用的高阶组件
由于HOC创建了新的抽象容器组件,因此以下是您通常可以使用它们执行的操作的列表:
- 将元素或组件包裹在组件周围。
- 状态抽象。
- 操作道具,例如添加新道具以及修改或删除现有道具。
- 验证创建道具。
- 使用引用访问实例方法。
让我们一一讨论。
HOC作为包装器组件
回想一下,我之前的教程中的最后一个示例演示了HOC如何将InputComponent与其他组件和元素包装在一起。 这对于样式化和在可能的情况下重用逻辑很有用。 例如,您可以使用此技术创建应由某些事件触发的可重用加载器指示器或动画过渡效果。
加载指示器HOC
第一个示例是使用HOC构建的加载指示器。 它检查特定道具是否为空,并显示加载指示符,直到获取并返回数据为止。
LoadIndicator / LoadIndicatorHOC.jsx
/* Method that checks whether a props is empty
prop can be an object, string or an array */
const isEmpty = (prop) => (
prop === null ||
prop === undefined ||
(prop.hasOwnProperty('length') && prop.length === 0) ||
(prop.constructor === Object && Object.keys(prop).length === 0)
);
const withLoader = (loadingProp) => (WrappedComponent) => {
return class LoadIndicator extends Component {
render() {
return isEmpty(this.props[loadingProp]) ? <div className="loader" /> : <WrappedComponent {...this.props} />;
}
}
}
export default withLoader;
LoadIndicator / LoadIndicatorDemo.jsx
import React, { Component } from 'react';
import withLoader from './LoaderHOC.jsx';
class LoaderDemo extends Component {
constructor(props) {
super(props);
this.state = {
contactList: []
}
}
componentWillMount() {
let init = {
method: 'GET',
headers: new Headers(),
mode: 'cors',
cache: 'default'
};
fetch
('https://demo1443058.mockable.io/users/', init)
.then( (response) => (response.json()))
.then(
(data) => this.setState(
prevState => ({
contactList: [...data.contacts]
})
)
)
}
render() {
return(
<div className="contactApp">
<ContactListWithLoadIndicator contacts = {this.state.contactList} />
</div>
)
}
}
const ContactList = ({contacts}) => {
return(
<ul>
{/* Code omitted for brevity */}
</ul>
)
}
/* Static props can be passed down as function arguments */
const ContactListWithLoadIndicator = withLoader('contacts')(ContactList);
export default LoaderDemo;
这也是我们第一次使用第二个参数作为HOC的输入。 第二个参数,我命名为“ loadingProp”,在这里用于告诉HOC,它需要检查特定道具是否已获取并可用。 在示例中, isEmpty
函数检查loadingProp
是否为空,并显示一个指示符,直到更新道具为止。
您有两个选项可以将数据向下传递到HOC,既可以作为道具(通常的方式),也可以作为HOC的参数。
/* Two ways of passing down props */
<ContactListWithLoadIndicator contacts = {this.state.contactList} loadingProp= "contacts" />
//vs
const ContactListWithLoadIndicator = withLoader('contacts')(ContactList);
这是我在两者之间进行选择的方式。 如果数据没有超出HOC的范围,并且数据是静态的,则将它们作为参数传递。 如果道具与HOC以及包装的组件相关,请将它们作为常规道具传递。 在第三篇教程中,我已经详细介绍了这一点。
状态抽象和道具操纵
状态抽象意味着将状态概括为高阶分量。 WrappedComponent
所有状态管理将由高阶组件处理。 HOC添加新状态,然后将该状态作为道具传递给WrappedComponent
。
高阶通用容器
如果您注意到,上面的加载器示例包含一个使用获取API发出GET请求的组件。 检索数据后,数据以状态存储。 挂载组件时发出API请求是一种常见的情况,我们可以制作一个完全适合此角色的HOC。
GenericContainer / GenericContainerHOC.jsx
import React, { Component } from 'react';
const withGenericContainer = ({reqUrl, reqMethod, resName}) => WrappedComponent => {
return class GenericContainer extends Component {
constructor(props) {
super(props);
this.state = {
[resName]: [],
}
}
componentWillMount() {
let init = {
method: reqMethod,
headers: new Headers(),
mode: 'cors',
cache: 'default'
};
fetch(reqUrl, init)
.then( (response) => (response.json()))
.then(
(data) => {this.setState(
prevState => ({
[resName]: [...data.contacts]
})
)}
)
}
render() {
return(
<WrappedComponent {...this.props} {...this.state} />)
}
}
}
export default withGenericContainer;
GenericContainer / GenericContainerDemo.jsx
/* A presentational component */
const GenericContainerDemo = () => {
return (
<div className="contactApp">
<ContactListWithGenericContainer />
</div>
)
}
const ContactList = ({contacts}) => {
return(
<ul>
{/* Code omitted for brevity */}
</ul>
)
}
/* withGenericContainer HOC that accepts a static configuration object.
The resName corresponds to the name of the state where the fetched data will be stored*/
const ContactListWithGenericContainer = withGenericContainer(
{ reqUrl: 'https://demo1443058.mockable.io/users/', reqMethod: 'GET', resName: 'contacts' })(ContactList);
状态已被概括,并且状态值作为props被传递。 我们也使该组件可配置。
const withGenericContainer = ({reqUrl, reqMethod, resName}) => WrappedComponent => {
}
它接受配置对象作为输入,以提供有关API URL,方法以及存储结果的状态键名称的更多信息。 componentWillMount()
使用的逻辑演示了如何将动态键名与this.setState
一起this.setState
。
高阶表格
这是另一个使用状态抽象来创建有用的高阶表单组件的示例。
CustomForm / CustomFormDemo.jsx
const Form = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
props.onSubmit();
}
const handleChange = (e) => {
const inputName = e.target.name;
const inputValue = e.target.value;
props.onChange(inputName,inputValue);
}
return(
<div>
{/* onSubmit and onChange events are triggered by the form */ }
<form onSubmit = {handleSubmit} onChange={handleChange}>
<input name = "name" type= "text" />
<input name ="email" type="text" />
<button type="submit"> Submit </button>
</form>
</div>
)
}
const CustomFormDemo = (props) => {
return(
<div>
<SignupWithCustomForm {...props} />
</div>
);
}
const SignupWithCustomForm = withCustomForm({ contact: {name: '', email: ''}})({propName:'contact', propListName: 'contactList'})(Form);
CustomForm / CustomFormHOC.jsx
const CustomForm = (propState) => ({propName, propListName}) => WrappedComponent => {
return class withCustomForm extends Component {
constructor(props) {
super(props);
propState[propListName] = [];
this.state = propState;
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
/* prevState holds the old state. The old list is concatenated with the new state and copied to the array */
handleSubmit() {
this.setState( prevState => {
return ({
[propListName]: [...prevState[propListName], this.state[propName] ]
})}, () => console.log(this.state[propListName]) )}
/* When the input field value is changed, the [propName] is updated */
handleChange(name, value) {
this.setState( prevState => (
{[propName]: {...prevState[propName], [name]:value} }) )
}
render() {
return(
<WrappedComponent {...this.props} {...this.state} onChange = {this.handleChange} onSubmit = {this.handleSubmit} />
)
}
}
}
export default withCustomForm;
该示例演示如何将状态抽象与演示组件一起使用,以使表单创建更加容易。 在此,表单是一个表示性的组成部分,是HOC的输入。 表单的初始状态和状态项的名称也作为参数传递。
const SignupWithCustomForm = withCustomForm
({ contact: {name: '', email: ''}}) //Initial state
({propName:'contact', propListName: 'contactList'}) //The name of state object and the array
(Form); // WrappedComponent
但是,请注意,如果有多个具有相同名称的道具,则顺序很重要,并且道具的最后声明将始终获胜。 在这种情况下,如果另一个组件推送了一个名为contact
或contactList
的道具,则将导致名称冲突。 因此,您应该为您的HOC道具命名空间,以免它们与现有道具冲突,或者以这样的方式对它们进行排序:应首先声明应具有最高优先级的道具。 在第三个教程中将对此进行深入介绍。
使用HOC进行道具操作
道具操纵涉及添加新道具,修改现有道具或完全忽略它们。 在上面的CustomForm示例中,HOC传递了一些新的道具。
<WrappedComponent {...this.props} {...this.state} onChange = {this.handleChange} onSubmit = {this.handleSubmit} />
同样,您可以决定完全忽略道具。 下面的示例演示了这种情况。
// Technically an HOC
const ignoreHOC = (anything) => (props) => <h1> The props are ignored</h1>
const IgnoreList = ignoreHOC(List)()
<IgnoreList />
您也可以使用此技术进行一些验证/过滤道具。 高阶组件决定子组件应接收某些道具,还是在不满足某些条件的情况下将用户带到其他组件。
用于保护路线的高阶组件
这是通过使用withAuth
高阶组件包装相关组件来保护路由的示例。
ProtectedRoutes / ProtectedRoutesHOC.jsx
const withAuth = WrappedComponent => {
return class ProtectedRoutes extends Component {
/* Checks whether the used is authenticated on Mount*/
componentWillMount() {
if (!this.props.authenticated) {
this.props.history.push('/login');
}
}
render() {
return (
<div>
<WrappedComponent {...this.props} />
</div>
)
}
}
}
export default withAuth;
ProtectedRoutes / ProtectedRoutesDemo.jsx
import {withRouter} from "react-router-dom";
class ProtectedRoutesDemo extends Component {
constructor(props) {
super(props);
/* Initialize state to false */
this.state = {
authenticated: false,
}
}
render() {
const { match } = this.props;
console.log(match);
return (
<div>
<ul className="nav navbar-nav">
<li><Link to={`${match.url}/home/`}>Home</Link></li>
<li><Link to={`${match.url}/contacts`}>Contacts(Protected Route)</Link></li>
</ul>
<Switch>
<Route exact path={`${match.path}/home/`} component={Home} />
<Route path={`${match.path}/contacts`} render={() => <ContactsWithAuth authenticated={this.state.authenticated} {...this.props} />} />
</Switch>
</div>
);
}
}
const Home = () => {
return (<div> Navigating to the protected route gets redirected to /login </div>);
}
const Contacts = () => {
return (<div> Contacts </div>);
}
const ContactsWithAuth = withRouter(withAuth(Contacts));
export default ProtectedRoutesDemo;
withAuth
检查用户是否已通过身份验证,否则,将用户重定向到/login.
我们使用了withRouter
,它是一个react-router实体。 有趣的是, withRouter
还是一个高阶组件,用于在每次渲染时将更新的匹配,位置和历史道具传递给包装的组件。
例如,它将历史对象作为道具推送,以便我们可以按以下方式访问该对象的实例:
this.props.history.push('/login');
您可以在官方的react-router文档中阅读有关withRouter
更多信息。
通过引用访问实例
React具有一个特殊的属性,您可以将其附加到组件或元素。 ref属性(ref代表参考)可以是附加到组件声明的回调函数。
挂载组件后,将调用该回调,并且您将获得所引用组件的实例作为回调的参数。 如果您不确定ref的工作方式,则有关Ref和DOM的官方文档将对此进行深入讨论。
在我们的HOC中,使用ref的好处是您可以获取WrappedComponent
的实例并从高阶组件中调用其方法。 这不是典型的React数据流的一部分,因为React更喜欢通过props进行通信。 但是,在许多地方您可能会发现此方法很有用。
RefsDemo / RefsHOC.jsx
const withRefs = WrappedComponent => {
return class Refs extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
}
this.setStateFromInstance = this.setStateFromInstance.bind(this);
}
/* This method calls the Wrapped component instance method getCurrentState */
setStateFromInstance() {
this.setState({
value: this.instance.getCurrentState()
})
}
render() {
return(
<div>
{ /* The ref callback attribute is used to save a reference to the Wrapped component instance */ }
<WrappedComponent {...this.props} ref= { (instance) => this.instance = instance } />
<button onClick = {this. setStateFromInstance }> Submit </button>
<h3> The value is {this.state.value} </h3>
</div>
);
}
}
}
RefsDemo / RefsDemo.jsx
const RefsDemo = () => {
return (<div className="contactApp">
<RefsComponent />
</div>
)
}
/* A typical form component */
class SampleFormComponent extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
}
this.handleChange = this.handleChange.bind(this);
}
getCurrentState() {
console.log(this.state.value)
return this.state.value;
}
handleChange(e) {
this.setState({
value: e.target.value
})
}
render() {
return (
<input type="text" onChange={this.handleChange} />
)
}
}
const RefsComponent = withRefs(SampleFormComponent);
ref
回调属性保存对WrappedComponent
的引用。
<WrappedComponent {...this.props} ref= { (instance) => this.instance = instance } />
this.instance
具有与一个参考WrappedComponent
。 现在,您可以调用实例的方法在组件之间传递数据。 但是,请仅在必要时谨慎使用此功能。
最终演示
我已将本教程中的所有示例都整合到一个演示中。 只需从GitHub克隆或下载源代码 ,您就可以自己尝试一下。
要安装依赖项并运行项目,只需在项目文件夹中运行以下命令。
npm install
npm start
摘要
这是第二部分有关高阶组件的教程的结尾。 今天,我们了解了许多不同的HOC模式和技术,并通过实际示例演示了如何在项目中使用它们。
在本教程的第三部分中,您可以期待应该了解的一些最佳实践和HOC替代方法。 敬请期待。 在评论框中分享您的想法。