从零开始学习 React 高阶组件

LawrenceQuintina 发布于1年前

01、介绍

  • React 高阶组件也叫做 React HOC(High Order Component), 它是react中的高级技术, 用来重用组件逻辑。
  • 但高阶组件本身并不是React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的。
  • 那么在学习高阶组件之前有一个概念我们必须清楚,就是高阶函数。

02、高阶函数

  • 概念:高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回
  • 举个栗子:

    • 接收函数作为参数

      function a(x) {
        x();
      }
      function b() {
        alert('hello');
      }
      
      a(b);
    • 将函数作为输出返回

      function a() {
        function b() {
          alert('hello');
        }
        return b;
      }
      
      a()();
  • 以上函数a就是一个高阶函数, 用法非常简单, 那么实际开发中又有哪些是高阶函数呢?

    • Array 的 map 、reduce 、filter 等方法
    • Object 的 keys 、values 等方法

03、高阶组件

  • 概念:高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
  • 举个栗子:

    // WrappedComponent 就是传入的包装组件
    function withHoc(WrappedComponent) {
      return class extends Component {
        render () {
          return (
            <WrappedComponent />
          )
        }
      }
    }
  • withHoc 函数就是一个高阶组件。那么高阶组件到底有什么神奇的魔力,值得我们为之着迷?
  • 开发组件时,我们会遇到相同的功能,使用高阶组件则能减少重复代码

04、高阶组件实训1

  • 目的: 定义高阶组件
  • 组件 Login -- 登陆页面

    // 受控组件
    class Login extends Component {
      state = {
        username: '',
        password: ''
      }
      
      onUsernameChange = (e) => {
        this.setState({username: e.target.value});
      }
      
      onPasswordChange = (e) => {
        this.setState({password: e.target.value});
      }
      
      login = (e) => {
        // 禁止默认事件
        e.preventDefault();
        // 收集表单数据
        const { username, password } = this.state;
        alert(`用户名: ${username}, 密码: ${password}`);
      }
      
      render () {
        const { username, password } = this.state;
        return (
          <div>
            <h2>登陆</h2>
            <form onSubmit={this.login}>
              用户名: <input type="text" name="username" value={username} onChange={this.onUsernameChange}/> <br/>
              密码: <input type="password" name="password" value={password} onChange={this.onPasswordChange}/> <br/>
              <input type="submit" value="登陆"/>
            </form>
          </div>
        )
      }
    }
  • 组件 Register -- 注册页面

    // 受控组件
    class Register extends Component {
      state = {
        username: '',
        password: '',
        rePassword: ''
      }
      
      onUsernameChange = (e) => {
        this.setState({username: e.target.value});
      }
      
      onPasswordChange = (e) => {
        this.setState({password: e.target.value});
      }
      
      onRePasswordChange = (e) => {
        this.setState({rePassword: e.target.value});
      }
      
      register = (e) => {
        // 禁止默认事件
        e.preventDefault();
        // 收集表单数据
        const { username, password, rePassword } = this.state;
        alert(`用户名: ${username}, 密码: ${password}, 确认密码: ${rePassword}`);
      }
      
      render () {
        const { username, password, rePassword } = this.state;
        return (
          <div>
            <h2>注册</h2>
            <form onSubmit={this.register}>
              用户名: <input type="text" name="username" value={username} onChange={this.onUsernameChange}/> <br/>
              密码: <input type="password" name="password" value={password} onChange={this.onPasswordChange}/> <br/>
              确认密码: <input type="password" name="rePassword" value={rePassword} onChange={this.onRePasswordChange}/> <br/>
              <input type="submit" value="注册"/>
            </form>
          </div>
        )
      }
    }
  • 页面效果
  • 我们发现里面重复逻辑实在太多了,尤其是 onXxxChange 函数出现太多,我们先优化一下。

    // 我们以 Register 组件为例来看
    class Register extends Component {
      state = {
        username: '',
        password: '',
        rePassword: ''
      }
      // 最终修改状态数据的函数
      onChange = (stateName, stateValue) => {
        this.setState({[stateName]: stateValue});
      }
      // 高阶函数 --> 这样后面就能一直复用当前函数,而不用重新创建了~
      composeChange = (name) => {
        return (e) => this.onChange(name, e.target.value);
      }
      // 统一所有提交表单函数名
      handleSubmit = (e) => {
        e.preventDefault();
        const { username, password, rePassword } = this.state;
        alert(`用户名: ${username}, 密码: ${password}, 确认密码: ${rePassword}`);
      }
      
      render () {
        const { username, password, rePassword } = this.state;
        return (
          <div>
            <h2>注册</h2>
            <form onSubmit={this.handleSubmit}>
              用户名: <input type="text" name="username" value={username} onChange={this.composeChange('username')}/> <br/>
              密码: <input type="password" name="password" value={password} onChange={this.composeChange('password')}/> <br/>
              确认密码: <input type="password" name="rePassword" value={rePassword} onChange={this.composeChange('rePassword')}/> <br/>
              <input type="submit" value="注册"/>
            </form>
          </div>
        )
      }
    }
  • 现在两个页面都有 onChange 、 composeChange 、handleSubmit 函数和相关的状态,我们接下来提取,封装成高阶组件

    // 高阶组件 withHoc
    export default function withHoc(WrappedComponent) {
      return class extends Component {
        state = {
          username: '',
          password: '',
          rePassword: ''
        }
      
        onChange = (stateName, stateValue) => {
          this.setState({[stateName]: stateValue});
        }
      
        composeChange = (name) => {
          return (e) => this.onChange(name, e.target.value);
        }
        
        handleSubmit = (e) => {
          e.preventDefault();
          const { username, password, rePassword } = this.state;
          if (rePassword) {
            alert(`用户名: ${username}, 密码: ${password}, 确认密码: ${rePassword}`);
          } else {
            alert(`用户名: ${username}, 密码: ${password}`);
          }
        }
        
        render () {
          // 抽取方法
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          // 将状态数据和操作的方法以 props 的方式传入的包装组件中
          return (
            <div>
              {/*提取公共头部*/}
              <h2>xxx</h2>
              <WrappedComponent {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
    
    // 组件 Register
    class Register extends Component {
      render () {
        const { handleSubmit, composeChange, username, password, rePassword } = this.props;
        return (
          <form onSubmit={handleSubmit}>
            用户名: <input type="text" name="username" value={username} onChange={composeChange('username')}/> <br/>
            密码: <input type="password" name="password" value={password} onChange={composeChange('password')}/> <br/>
            确认密码: <input type="password" name="rePassword" value={rePassword} onChange={composeChange('rePassword')}/> <br/>
            <input type="submit" value="注册"/>
          </form>
        )
      }
    }
    // 向外暴露的是高阶组件的返回值~包装了 Register 组件返回了一个新组件
    export default withHoc(Register);
  • 现在我们提取了公共方法、状态等数据, 封装了一个基本的高阶组件。 但是还有很多需要问题需要解决,现在开始行动~

05、高阶组件实训2

  • 目的: 向高阶组件中传参
  • 修改高阶组件

    // 再次包裹了一层高阶函数, 这个高阶函数执行后返回值才是高阶组件
    // 通过这种方式, 高阶组件内部就能获取参数了~
    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        ...重复代码省略...
        
        render () {
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          return (
            <div>
              {/*获取到参数值就能正常显示了~*/}
              <h2>{title}</h2>
              <WrappedComponent {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
  • 在 Login / Register 组件中使用

    • export default withHoc('登陆')(Login);
    • export default withHoc('注册')(Register);

06、高阶组件实训3

  • 目的: 获取父组件传递的 props
  • 修改 App 组件

    class App extends Component {
      render() {
        return (
          <div>
            {/*父组件向子组件传递属性*/}
            <Login name="jack" age={18}/>
            <Register />
          </div>
        );
      }
    }
  • 修改高阶组件

    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        ...重复代码省略...
        
        render () {
          const mapMethodToProps = {
            composeChange: this.composeChange,
            handleSubmit: this.handleSubmit,
          }
          return (
            <div>
              {/*获取到参数值就能正常显示了~*/}
              <h2>{title}</h2>
              {/* 将当前组件接受到的props传给包装组件~*/}
              <WrappedComponent {...this.props} {...this.state} {...mapMethodToProps}/>
            </div>
          )
        }
      }
    }
  • Login 组件中使用

     class Login extends Component {
       render () {
         const { handleSubmit, composeChange, username, password, name, age } = this.props;
         return (
           <div>
             <p>你的名字: {name}</p>
             <p>你的年龄: {age}</p>
             <form onSubmit={handleSubmit}>
               用户名: <input type="text" name="username" value={username} onChange={composeChange('username')}/> <br/>
               密码: <input type="password" name="password" value={password} onChange={composeChange('password')}/> <br/>
               <input type="submit" value="登陆"/>
             </form>
           </div>
         )
       }
     }

07、高阶组件实训4

  • 目的: 修改在 React-devtool 中高阶组件名称,方便调试
  • 修改高阶组件

    export default (title) => (WrappedComponent) => {
      return class Form extends Component {
        // 定义静态方法,修改组件在调试工具中显示的名称
        static displayName = `Form(${getDisplayName(WrappedComponent)})`
        
        ...省略重复代码...
      }
    }
    // 获取包装组件的displayName的方法
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
  • 修改之前名称

  • 修改之后名称

08、使用装饰器

  • 目的: 简化使用高阶组件
  • 下载包

    • npm i react-app-rewired customize-cra @babel/plugin-proposal-decorators -D
  • 在项目根目录配置 config-overrides.js

    const { override, addBabelPlugins } = require('customize-cra');
    // 修改 create-react-app 的 webpack 的配置
    module.exports = override(
      // 添加 babel 插件
      addBabelPlugins(
        [
          "@babel/plugin-proposal-decorators",
          {
            "legacy": true
          }
        ]
      )
    )
  • 修改 package.json 的 scripts

    // 将 react-scripts 修改为 react-app-rewired
    "scripts": {
      "start": "react-app-rewired start",
      "build": "react-app-rewired build",
      "test": "react-app-rewired test"
    },
  • 以上就是使用 decorator 的配置,修改完后就能使用了~
  • 修改 Login 组件

    @withHoc('登陆')
    class Login extends Component {
      ...省略重复代码...
    }
    export default Login;
  • 修改 Register 组件

    @withHoc('注册')
    class Register extends Component {
      ...省略重复代码...
    }
    export default Register;
  • react-app-rewired customize-cra 是 create-react-app 2.0以上专门用来修改 webpack 的配置
  • decorator 还能做很多事,感兴趣朋友可以看看 阮一峰ES6教程 了解更多
重复代码永远是我们需要考虑处理的代码,所以我们有模块化、组件化、工具类函数等等,
在 React 中再次引入了一个高阶组件的概念,都是为了去除掉万恶的重复代码,让我们代码变得更加精简。
本篇文章所有源码都放在了 git仓库,如果它对你有帮助的话,欢迎点 star ~~

查看原文: 从零开始学习 React 高阶组件

  • brownswan
  • greenfrog
  • heavyfrog
  • greencat
  • BartholomewArvin
  • NoelZebulon
  • GibsonMartha