Higher Order Components (HOC) Demo:滑鼠移入顯示提示文字

Tooltip demo

回顧

Result.js

轉門處理卡片的 UI

將要顯示提示文字的地方用 <Tooltip></Tooltip> 包起來

// Results.js

import Tooltip from './Tooltip';

function ProfileList ({ profile }) {
  return (
    <ul className='card-list'>
      <li>
        <Tooltip text="User's location">
          <FaCompass color='rgb(144, 115, 255)' size={22} />
          {profile.location}
        </Tooltip>
      </li>
      <li>
        <FaUsers color='rgb(129, 195, 245)' size={22} />
        {profile.followers.toLocaleString()} followers
      </li>
      ...
      ...
    </ul>
  )
}

export default class Results extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      winner: null,
    }
  }

  render() {
    return (
      <Card>
        <ProfileList profile={this.state.winner.profile} />
      </Card>
    )
  }
}

Tooltip.js

專門處理 Tooltip 的顯示邏輯和 UI

滑鼠移入顯示提示文字,移出提示消失

// Tooltip.js

const styles = {
  container: {
    position: 'relative',
    display: 'flex'
  },
  tooltip: {
    boxSizing: 'border-box',
    position: 'absolute',
    width: '160px',
    bottom: '100%',
    left: '50%',
    marginLeft: '-80px',
    borderRadius: '3px',
    backgroundColor: 'hsla(0, 0%, 20%, 0.9)',
    padding: '7px',
    marginBottom: '5px',
    color: '#fff',
    textAlign: 'center',
    fontSize: '14px',
  }
}

export default class Tooltip extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      hovering: false,
    }

    this.mouseOver = this.mouseOver.bind(this);
    this.mouseOut = this.mouseOut.bind(this);
  }

  mouseOver() {
    this.setState({ hovering: true })
  }

  mouseOut() {
    this.setState({ hovering: false })
  }

  render() {
    const { hovering } = this.state;
    const { text, children } = this.props;

    return (
      <div
        onMouseOver={this.mouseOver}
        onMouseOut={this.mouseOut}
        style={styles.container}
      >
        {hovering && <div style={styles.tooltip}>{text}</div>}
        {children}
      </div>
    )
  }
}

有可能再進一步優化嗎?

我已經將卡片和 Tooltip 的邏輯、畫面拆開成獨立的元件,還能再做什麼優化?

如果 mouseover 和 mouseout 功能,不只用在 Tooltip,也許用在圖片上或全站其他地方,是否可以把這塊邏輯額外抽離出來,寫成獨立的元件,可避免減少重複的程式碼?!

withHover Higher Order Component

目標

  1. Tooltip.js:把 mouseover & mouseout 相關功能抽取出來,另外新增在 withHover.js 專門處理。

  2. Tooltip.js:只保留 UI。

  3. 使用 Higher-Order Components (HOC) 來做優化。

改寫 Tooltip.js

  1. 移除 mouseover & mouseout 相關邏輯

    沒有 state 的存在,代表可以將 Tooltip 改寫成 Functional component,讓 Tooltip 專門只處理 UI。

  2. 新增 export default withHover(Tooltip)

    等下會建立 function withHover (專門處理 mouseover 邏輯),先將 Tooltip 當作 argument 傳入。

// Tooltip.js

import withHover from './withHover';

const styles = {
  container: {
    ...
  },
  tooltip: {
    ...
  }
}

// 寫成 Functional component,移除 mouseover 邏輯,專心只顧 UI
function Tooltip({ text, children, hovering }) {
  return (
    <div style={styles.container}>
      {hovering && <div style={styles.tooltip}>{text}</div>}
      {children}
    </div>
  )
}

// 等下會建立 function withHover,專門處理 mouseover 邏輯
// 把元件 Tooltip 當作 argument 傳入 withHover
export default withHover(Tooltip);

新增 withHover.js

專門處理全站 mouseover & mouseout ,使用 Higher-Order Component (HOC) 把邏輯額外拆出來,可以在全站不同區塊重複利用。

補充:什麼是 Higher-Order Component
- Is a component
- Takes in a component as an argument (ex. 在這裡傳入元件 Tooltip)
- Returns a new component WithHover
- The component it returns can render the original component (Tooltip) that was passed in

function higherOrderComponent (Component) {
  return class extends React.Component {
    render() {
      return <Component />
    }
  }
}
// withHover.js

/**
  * 專門處理全站 mouseover & mouseout 的邏輯
  * 額外拆出來,可以在全站不同區塊重複利用
  * 上半部保留 mouse 的邏輯,下半部 render 引入 <Tooltip> 的 UI
  */
export default function withHover (Component) {
  return class WithHover extends React.Component {
    constructor(props) {
      super(props);

      this.state = {
        hovering: false,
      }

      this.mouseOver = this.mouseOver.bind(this);
      this.mouseOut = this.mouseOut.bind(this);
    }

    mouseOver() {
      this.setState({ hovering: true })
    }

    mouseOut() {
      this.setState({ hovering: false })
    }

    render() {
      return (
        <div
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        >
          {/* 引入要使用 mouseover 功能的 Component Tooltip */}
          <Component hovering={this.state.hovering} />
        </div>
      )
    }
  }
}


// Tooltip.js
export default withHover(Tooltip)

mouseover 相關的邏輯已經寫好,要怎麼將 Component (Tooltip)引入?

div 裡引入 <Component>,將 this.state.hovering 透過 props 傳入,這樣 Tooltip 才能接收到 Boolean 值,當 hovering = true 時顯示提示文字。

Tooltip 無法接收到 props

到這步會以為功能應該就完成了,打開前端頁面會發覺 Console 出現 Error,props 回傳的值顯示 undefined,應該顯示 Tooltip 的元素也消失在頁面上,如下圖。

解析 <Tooltip> 開始執行時,到底回傳了什麼東西?

  1. 先看哪裡 import

檔案顯示 import 檔案的源頭在 ./Tooltip

// Results.js

import Tooltip from './Tooltip';

function ProfileList ({ profile }) {
  return (
    <ul className='card-list'>
      <li>
        <Tooltip text="User's location">
          <FaCompass color='rgb(144, 115, 255)' size={22} />
          {profile.location}
        </Tooltip>
      </li>
      <li>
        <FaUsers color='rgb(129, 195, 245)' size={22} />
        {profile.followers.toLocaleString()} followers
      </li>
      ...
      ...
    </ul>
  )
}
  1. 打開 Tooltip.js 再觀察 export

結果只是呼叫執行另一個 function withHover(Tooltip)

// Tooltip.js

import withHover from './withHover';

function Tooltip({ text, children, hovering }) {
  return (
    <div style={styles.container}>
      {hovering && <div style={styles.tooltip}>{text}</div>}
      {children}
    </div>
  )
}

export default withHover(Tooltip);
  1. 繼續打開 withHover.js

withHover(Tooltip) 被呼叫執行後,其實回傳的是 class WithHover extends React.Component,也就是說,任何傳入 <Tooltip>props 值,其實是傳到 WithHover 這個元件裡,結果造成 props 並未真的傳入 <Tooltip>

// withHover.js

export default function withHover (Component) {
  return class WithHover extends React.Component {
    constructor(props) {
      ...
    }

    mouseOver() {
      this.setState({ hovering: true })
    }

    mouseOut() {
      this.setState({ hovering: false })
    }

    render() {
      console.log(this.props)
      // {
      //   children: Array,
      //   text: String
      // }

      return (
        <div
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        >
          {/* 引入要使用 mouseover 功能的 Component Tooltip */}
          <Component hovering={this.state.hovering} />
        </div>
      )
    }
  }
}


顯示 WIthHover 是有接收到 的 props

解決 <Tooltip> 無法接收 props

把傳到 WithHoverprops,在 render 裡繼續傳到 Tooltip Component。

<Component> 裡加入 {...this.props} 就能把所有 props 再轉傳到 <Tooltip>

// withHover.js

export default function withHover (Component) {
  return class WithHover extends React.Component {
    constructor(props) {
      ...
    }

    mouseOver() {
      this.setState({ hovering: true })
    }

    mouseOut() {
      this.setState({ hovering: false })
    }

    render() {
      return (
        <div
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        >
          <Component {...this.props} />
        </div>
      )
    }
  }
}

補充:解決命名衝突

萬一 hovering 這個 props 同時也在 <Tooltip> 裡使用怎麼辦? 除了手動更改命名外,我們可以讓使用者自行命名想要傳入的 props。

// withHover.js
export default function withHover (Component, propName = 'hovering') {
  return class WithHover extends React.Component {
    ...

    render() {
      // propName: 使用者可自行改名,或者不傳參數使用預設的 'hoveing'
      // 把 Tooltip 需要的 3 個 props 帶入:hovering, text, children
      const props = {
        [propName]: this.state.hovering,
        ...this.props
      }

      return (
        <div
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        >
          <Component {...props} />
        </div>
      )
    }
  }
}

// Tooltip.js
import withHover from './withHover';

function Tooltip({ text, children, isHovering }) {
  return (
    <div style={styles.container}>
      {isHovering && <div style={styles.tooltip}>{text}</div>}
      {children}
    </div>
  )
}

// 第二個 argument 傳入想把預設 hovering 改掉的新名字
export default withHover(Tooltip, 'isHovering');


hovering 順利改成 isHovering

1個讚