这是高阶组件 (HOC) 系列的第二部分。今天,我将介绍有用且可实现的不同高阶组件模式。使用 HOC,您可以将冗余代码抽象为更高阶的层。然而,就像其他任何模式一样,习惯 HOC 需要一些时间。本教程将帮助您弥合这一差距。
先决条件
如果您还没有,我建议您阅读本系列的第一部分。在第一部分中,咱们讨论了 HOC 语法基础以及开始使用高阶组件所需的一切。
在本教程中,咱们将在第一部分中已经介绍的概念之上进行构建。我创建了几个实际有用的示例 HOC,您可以将这些想法融入您的项目中。每个部分都提供了代码片段,教程末尾提供了本教程中讨论的所有实用 HOC 的工作演示。
你也可以从我的GitHub 仓库fork 代码。
实用的高阶组件
由于 HOC 创建了一个新的抽象容器组件,以下是您通常可以使用它们执行的操作列表:
将元素或组件包裹在组件周围。
状态抽象。
操作道具,例如添加新道具以及修改或删除现有道具。
要创建的道具验证。
使用 refs 访问实例方法。
让咱们一一谈谈。
HOC 作为包装器组件
如果您还记得,我之前教程中的最后一个示例演示了 HOC 如何将 InputComponent 与其他组件和元素一起包装。这对于样式化和尽可能重用逻辑很有用。例如,您可以使用这种技术来创建一个可重用的加载器指示器或一个应该由某些事件触发的动画过渡效果。
加载指示器 HOC
第一个示例是使用 HOC 构建的加载指示器。它检查特定的道具是否为空,并显示加载指示器,直到数据被fetch ed 并返回。
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
为空,并显示一个指示符,直到 props 更新。
将数据传递给 HOC 有两种选择,一种是作为 prop(这是通常的方式),另一种是作为 HOC 的参数。
/* Two ways of passing down props */ <ContactListWithLoadIndicator contacts = {this.state.contactList} loadingProp= "contacts" /> //vs const ContactListWithLoadIndicator = withLoader('contacts')(ContactList);
以下是我如何在两者之间进行选择。如果数据没有超出 HOC 的范围,并且数据是静态的,则将它们作为参数传递。如果 props 与 HOC 以及被包装的组件相关,则将它们作为通常的 props 传递。我在第三篇教程中对此进行了更多介绍。
状态抽象和道具操作
状态抽象意味着将状态推广到更高阶的组件。的所有状态管理都WrappedComponent
将由高阶组件处理。HOC 添加新状态,然后将状态作为道具传递给WrappedComponent
.
高阶通用容器
如果你注意到了,上面的加载器示例有一个使用 fetch 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);
状态已被泛化,状态的价值正在作为道具传递下去。咱们也使组件可配置。
const withGenericContainer = ({reqUrl, reqMethod, resName}) => WrappedComponent => { }
它接受一个配置对象作为输入,提供有关 API URL、方法和存储结果的状态键名称的更多信息。中使用的逻辑componentWillMount()
演示使用带有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
or的 prop 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.
We've used withRouter
,这是一个 react-router 实体。有趣的是, 它也是一个高阶withRouter
组件,用于在每次渲染时将更新的匹配、位置和历史道具传递给被包装的组件。
例如,它将历史对象作为道具推送,以便咱们可以访问该对象的实例,如下所示:
this.props.history.push('/login');
您可以withRouter
在官方react-router 文档中了解更多信息。
通过 Refs 访问实例
React 有一个特殊的属性,你可以将它附加到组件或元素上。ref 属性(ref 代表引用)可以是 附加到组件声明的回调函数。
挂载组件后会调用回调,并且您会获得引用组件的实例作为回调的参数。如果你不确定 refs 是如何工作的,关于Refs 和 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
- LoadIndicator/LoadIndicatorHOC。jsx
- LoadIndicator/LoadIndicatorDemo.jsx
- 高阶通用容器
- GenericContainer/GenericContainerHOC.jsx
- GenericContainer/GenericContainerDemo.jsx
- 高阶形式
- CustomForm/CustomFormDemo.jsx
- CustomForm/CustomFormHOC.jsx
- 用于保护路由的高阶组件
- ProtectedRoutes/ProtectedRoutesHOC.jsx
- ProtectedRoutes/ProtectedRoutesDemo.jsx
- RefsDemo/RefsHOC.jsx
- RefsDemo/RefsDemo.jsx