React 進階之選擇合適的元件型別

HelKyle發表於2018-11-05

最近專案基本都是用 React,今天總結分享 React Component 常見的幾種形式,如果你在寫 React 時經常不知道怎麼拆分程式碼,這篇文章或許對你有所幫助。

原文連結: w3ctrain.com/2018/11/05/…

為了更充分理解 React,先搞懂平時寫的 JSX 是什麼。初學的時候有比較大困惑,這是一門新語言嗎?大部分人是匆匆掃過文件就開始開發。通過 babel-presets-react 處理能看到,其實 JSX 只是語法糖,最終在瀏覽器跑的還是 JS。React Component 最終都通過 React.createElement 建立。總之,寫 React 其實就是在寫 JS

jsx

SFC (Stateless Functional Component)

React 可以使用 Function 來建立 Component,這類 Component 沒有 lifecycle, 內部不維護 state,只要傳入的 props 有變化則進行重新渲染。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
複製程式碼

用箭頭函式的寫法還更加簡潔。

const Welcome = props => <h1>Hello, {props.name}</h1>;
複製程式碼

上面兩種形式生成 es5 程式碼都是一樣的。

var Welcome = function Welcome(props) {
  return _react2.default.createElement(
    "h1",
    null,
    "Hello, ",
    props.name
  );
};
複製程式碼

SFC 的特點是純粹只做 render,程式碼簡短沒有其他條件分支,並且相比 class Component 編譯後的程式碼量會少一些。

尷尬的是,在 React 16.7 react hooks 出來之後,SFC 這個名字有歧義了,因為用上 useState,SFC 也可以有 local state, 同樣可以擁有 lifecycle。再稱之為 Stateless Components 就很尷尬,改名叫 FC ?

HOC (Higher-Order Components)

高階元件對於 Vue 開發者來說應該是個陌生的概念(不知道,我用 Vue 的時候沒見過類似的用法)。從程式碼上看,高階元件就是一個方法,傳入一個元件,返回另一個元件。

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
複製程式碼

最常見的高階元件是 react-redux 裡面的 connect 方法,通過傳入 元件和 map*ToProps 方法,讓元件和 store 連線。元件內部就可以直接通過 props 獲得 connect 之後的值。

exprot default connect(
  mapStateToProps,
  mapDispatchToProps,
)(Component);
複製程式碼

高階元件適合用來擴充套件功能,把這部分功能從業務元件中抽離出來,需要的套上,不需要的時候移除,對被包裹元件侵入性非常小。

Dynamic Component

有些業務場景下,在執行時才能確定具體的標籤或者元件是什麼。在 React 的世界裡面,以大寫字母開頭會被當成動態元件載入,而小寫字母開頭會被認為是 HTML DOM tag。

// Heading.js
render() {
    const { tag: Tag, children } = this.props;
    return <Tag>{ children }</Tag>
}
複製程式碼

根據萬物皆為 JS 理論,只要傳入不同的 tag 標籤,就會渲染出不同的 heading 標籤。

heading

我們常用這種方式,在後端配置元件和資料,前端讀取配置之後渲染出對應的內容。

FaCC(Functions as Child Components)

React children 還可以是 Function 型別,如果直接呼叫它會什麼寫法? 比如封裝一個 Loading 元件,會給 children 提供 loading 引數,業務元件再根據 loading 判斷需要 render 什麼內容。

class LoadArea extends Component {
  state = {
    loading: true,
  };

  componentDidMount() {
    asyncFunc()
        .then(() => {
            this.setState({
              loading: false,
            })
        })
        .catch(() => {
            this.setState({
              loading: false,
            })
        })
  }

  render() {
    return (
      <React.Fragment>
        {this.props.children({
          ...this.props,
          ...this.state,
        })}
      </React.Fragment>
    );
  }
}
複製程式碼

用法

render() {
    <LoadingArea>
        ({ loading }) => {
            loading
                ? <Wating />
                : <Main />
        }
    </LoadingArea>
}
複製程式碼

同樣的,最終執行時都是 JS,沒有什麼好奇怪的。

React 16.* 新版本的 Conext.Consumer 就是採用了這種寫法。

render() {
    <ThemeContext.Provider value={this.state.theme}>
      ...
        <ThemeContext.Consumer>
          {({theme}) => (
            <button
              style={{backgroundColor: theme.background}}>
              Toggle Theme
            </button>
          )}
        </ThemeContext.Consumer>
      ...
    </ThemeContext.Provider>    
}

複製程式碼

再以最近開發的例子,分享元件拆分的好處。

需求:開發倒數計時元件,運營配置倒數計時結束時間,倒數計時初始化時間從服務端獲取,結束之前顯示倒數計時,倒數計時結束之後做對應的操作,比如切換倒數計時為其他元件。

元件拆分:

  • 一個業務層容器元件,負責統籌,處理業務邏輯。(統籌規劃,倒數計時完要幹什麼,你直接跟我說)
  • 一個通用‘倒數計時’的元件,向服務端輪詢系統時間,計算當前剩餘時間,FaCC 的形式提供給 children。(我數我的羊,你們愛幹嘛幹嘛)
  • 一個倒數計時UI元件,對剩餘時間格式化以及 UI 展示。(剩下多少,時間怎麼來的,與我無關,給我什麼我展示什麼)

虛擬碼:

// CountDownContainer.js
render() {
    const {
      endTime,
      renderSomethingAfterCountDown,
    } = this.props;

    return (
      <TimeLeftProvider endTime={endTime} >
        {seconds => (
          seconds > 0
            ? <CountDown {...this.props} remainingSeconds={seconds} />
            : renderSomethingAfterCountDown()
        )}
      </TimeLeftProvider>
    );
}
複製程式碼
// TimeLeftProvider.js
export default class TimeLeftProvider extends PureComponent {
  static propTypes = {
    children: PropTypes.func,
    endTime: PropTypes.number,
  }

  // ...

  componentDidMount() {
    this.poll();
  }

  poll() {
    queryServerTime();
    this.pollTimer = setInterval(() => {
      queryServerTime();
    }, pollInterval * 1000);
  }

  countDown() {
    setInterval(() => {
      this.setState(prevState => ({
        remainingSeconds: prevState.remainingSeconds - 1,
      }));
    }, 1000);
  }

  render() {
    const { remainingSeconds, reliable } = this.state;
    return this.props.children(remainingSeconds, reliable);
  }
}
複製程式碼
// CountDown.js

function CountDown(props) {
    const {
      remainingSeconds,
    } = props;
    const numbers = formatSeconds(remainingSeconds);
    const inputs = ['days', 'hours', 'minutes', 'seconds'];

    return (
      <div styleName={cls}>
        {
          inputs.map(key => ({
            label: key,
            number: numbers[key],
          })).map(
             //...
          )
        }
      </div>
    );
}
複製程式碼

最終得到的結果是:

Count Down

與此同時

  • 程式碼結構清晰,元件之間各司其職。
  • 元件可複用性強。
  • 單元測試簡單,每個元件都只測試自身的邏輯。

推薦閱讀

相關文章