React.js: Higher-Order Components (HOC)

React.js

Higher-Order Components(HOC)是一個函數,可代入元件(Component)作為參數,並回傳一個新的元件。使用 HOC 的目的是將通用的邏輯放在 HOC 中,變動的部分就由代入 Component 的 props 和 state 傳入即可。

簡易範例可參考這裡

範例

這是我的 Side Project「吃什麼,どっち」,主要用於推薦餐廳。其中,右側動態消息「有什麼新鮮事?」(Feeds,紫框處),且每個商店資訊的下方會顯示評價(Comments,藍框處)。Feeds 和 Comments 的功能很類似,都會展示一連串的資料,並且當使用者按下「看更多」按鈕時會顯示更多的項目。

吃什麼,どっち

HOC

HOC 接收一個元件(WrappedComponent)、狀態(state)等,這裡除了接收元件和狀態外,還有提取更多項目資料的方法(fetchDataFn)。

在此,將 Feeds 和 Comments 的通用邏輯提取出來,放在 HOC 中。它們共通的地方是

另外,由於每次提取資料要告知後端分頁號碼,因此會在 state 設定一個此元件專用的狀態 pageNum,並在每次提取資料完成後加一。每次提取資料後,會把新拿到的資料與前一份資料合併,當狀態更新重繪畫面時,就會將新拿到的資料附加在舊資料之後。

備註

// HOC.js

import React, { Component } from 'react';

const HOC = (WrappedComponent, state, fetchDataFn) => class extends Component {
  constructor(props) {
    super(props);
    this.state = {
      ...state,
      pageNum: 1
    };
    this.fetchDataHadler = this.fetchDataHadler.bind(this);
  }

  fetchDataHadler() {
    const fetchedData = fetchDataFn(this.state.pageNum);

    this.setState({
      pageNum: this.state.pageNum + 1,
      data: this.state.data.concat(fetchedData)
    });
  }

  render() {
    return (
      <div>
        <WrappedComponent {...this.props} {...this.state} /> // 註一
        <Button onClick={this.fetchDataHadler}>看更多</Button> // 註二
      </div>
    )
  }
};

export default HOC;

Feeds

來看右側動態消息「有什麼新鮮事?」(Feeds),這個元件從父層 props 得到資料並代入樣版產出畫面。

右側動態消息「有什麼新鮮事?」(Feeds)

// Feeds.js

import React from 'react';

const Feeds = ({ data }) => {
  return(
    <Feed>
      有什麼新鮮事?
      {
        _.map(data, (item, index) => {
          return (
            <Feed.Event key={index}>
              <Feed.Label><img src={item.image} /></Feed.Label>
              <Feed.Content>
                <Feed.Summary>
                  <Feed.User>{item.name}</Feed.User>在「{item.store.name」享受美食                  <Feed.Date>{item.time}</Feed.Date>
                </Feed.Summary>
              </Feed.Content>
            </Feed.Event>
          )
        })
      }
    </Feed>
  )
}

export default Feeds;

Comments

來看每個商店資訊的下方會顯示評價(Comments),這個元件從父層 props 得到資料並代入樣版產出畫面。

每個商店資訊的下方會顯示評價(Comments)

// Comments.js

import React from 'react';

const Comments = ({ data }) => {
  return (
    <Comment.Group>
      <Header>評價</Header>
      {
        _.map(data, (item, index) => {
          return (
            <Comment key={index}>
              <Comment.Avatar src={item.image} />
              <Comment.Content>
                {item.name}
                <Comment.Metadata>{item.time}</Comment.Metadata>
                <Comment.Text>{item.text}</Comment.Text>
              </Comment.Content>
            </Comment>
          )
        })
      }
    </Comment.Group>
  );
}

export default Comments;

怎麼用

最後來看要怎麼用,這裡分為兩部份

其中,feedsData 的資料格式範例如下。

const feedsData = {
  data: [
    {
      id: '123456789',
      name: '陳秋心',
      image: 'http://sample-image/123456789',
      time: '今天下午 3:30',
      store: {
        id: 'abc-123-def-456',
        name: 'Grab a Bite 幸福提食'
      }
    },
    ...
  ]
};
// Main.js

import React, { Component } from 'react';
import StoreItem from './components/StoreItem';
import HOC from './components/HOC';
import Feeds from './components/Feeds';

const fetchFeeds = (pageNum = 0) => {
  // 取得更多 feeds 資料...
};

const FeedList = HOC(Feeds, feedsData, fetchFeeds);

class StoreList extends Component {
  // 省略無關的部份...

  // 產出商店資訊
  renderStores() {
    return _.map(this.props.stores, store => {
      return (
        <StoreItem key={store.id} store={store} />
      );
    });
  }

  render() {
    return(
      <div>
        <Header />
        <Newsticker news={news} />
        { this.renderStores() }
        <TagList tags={defaultHotTags}/>
        <FeedList />
        <Footer />
      </div>
    )
  }
}

commentsData 的資料格式範例如下。

const commentsData = {
  storeId: store.id,
  data: [
    {
      id: '123456789',
      name: '吳艾月',
      image: 'http://sample-image/123456789',
      time: '今天下午 5:42',
      text: '超好吃!'
    },
    ...
  ]
};

在 StoreItem 載入元件 Comments。

// StoreItem.js

import React from 'react';
import HOC from './components/HOC';
import Comments from './components/Comments';

const StoreItem = ({store}) => {
  // 無關的部份省略...

  const fetchComments = () => {
    // 取得更多 comments 資料...
  };

  const CommentList = HOC(Comments, commentsData, fetchComments);

  return(
    <Item key={store.id}>
      <Item.Image src={store.image.url} />
      <Item.Content>
        <Item.Header>{store.name}</Item.Header>
        <Item.Meta>{store.description}</Item.Meta>
        <Item.Description>
          <ul>
            <li>地址:{store.location.address}</li>
            <li>電話:{store.phone}</li>
            <li>營業時間:{store.openingHour.start} ~ {store.openingHour.end}</li>
            <li>價位:{store.price.lowest} ~ {store.price.highest}</li>
            <li>網站介紹:<Link to={store.sns.website} title={store.name} target="_blank">{store.name}</Link></li>
          </ul>
        </Item.Description>
        <Item.Extra>
          {
            store.tags.map((item, index) => (
              <Label key={ index }>
                <Link to={`/tags/${item}`}>{ item }</Link>
              </Label>
            ))
          }
          <CommentList />
        </Item.Extra>
      </Item.Content>
    </Item>
  )
};

export default StoreItem;

總結

Higher-Order Components(HOC)是元件邏輯重用的進階技巧,它有以下優點

因此更能做到元件的重用,不必再撰寫相似程式碼了 ♥(´∀` )人

References

More


comments powered by Disqus