回顧
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
目標
-
Tooltip.js
:把 mouseover & mouseout 相關功能抽取出來,另外新增在withHover.js
專門處理。 -
Tooltip.js
:只保留 UI。 -
使用 Higher-Order Components (HOC) 來做優化。
改寫 Tooltip.js
-
移除 mouseover & mouseout 相關邏輯
沒有
state
的存在,代表可以將 Tooltip 改寫成 Functional component,讓 Tooltip 專門只處理 UI。 -
新增
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 componentWithHover
- 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>
開始執行時,到底回傳了什麼東西?
- 先看哪裡 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>
)
}
- 打開
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);
- 繼續打開
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
把傳到 WithHover
的 props
,在 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