React元件設計技巧

樓蘭小騎士發表於2019-02-16

React元件設計

元件分類

展示元件和容器元件

展示元件 容器元件
關注事物的展示 關注事物如何工作
可能包含展示和容器元件,並且一般會有DOM標籤和css樣式 可能包含展示和容器元件,並且不會有DOM標籤和css樣式
常常允許通過this.props.children傳遞 提供資料和行為給容器元件或者展示元件
對第三方沒有任何依賴,比如store 或者 flux action 呼叫flux action 並且提供他們的回撥給展示元件
不要指定資料如何載入和變化 作為資料來源,通常採用較高階的元件,而不是自己寫,比如React Reduxconnect(), Relay的createContainer(), Flux UtilsContainer.create()
僅通過屬性獲取資料和回撥 null
很少有自己的狀態,即使有,也是自己的UI狀態 null
除非他們需要的自己的狀態,生命週期,或效能優化才會被寫為功能元件 null

下面是一個可能會經常寫的元件,評論列表元件,資料互動和展示都放到了一個元件裡面。

// CommentList.js
class CommentList extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: `json`,
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <ul> {this.state.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

我們對上面的元件進行拆分,把他拆分成容器元件 CommentListContainer.js 和展示元件 CommentList

// CommentListContainer.js
class CommentListContainer extends React.Component {
  constructor() {
    super();
    this.state = { comments: [] }
  }
  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: `json`,
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  }
  render() {
    return <CommentList comments={this.state.comments} />;
  }
}


// CommentList.js
class CommentList extends React.Component {
  constructor(props) {
    super(props);
  }
  render() { 
    return <ul> {this.props.comments.map(renderComment)} </ul>;
  }
  renderComment({body, author}) {
    return <li>{body}—{author}</li>;
  }
}

優勢:

  • 展示和容器更好的分離,更好的理解應用程式和UI
  • 重用性高,展示元件可以用於多個不同的state資料來源
  • 展示元件就是你的調色盤,可以把他們放到單獨的頁面,在不影響應用程式的情況下,讓設計師調整UI
  • 迫使你分離標籤,達到更高的可用性

有狀態元件和無狀態元件

下面是一個最簡單的無狀態元件的例子:

function HelloComponent(props, /* context */) {
  return <div>Hello {props.name}</div>
}
ReactDOM.render(<HelloComponent name="Sebastian" />, mountNode)

可以看到,原本需要寫“類”定義(React.createClass 或者 class YourComponent extends React.Component)來建立自己元件的定義(有狀態元件),現在被精簡成了只寫一個 render 函式。更值得一提的是,由於僅僅是一個無狀態函式,React 在渲染的時候也省掉了將“元件類” 例項化的過程。

結合 ES6 的解構賦值,可以讓程式碼更精簡。例如下面這個 Input 元件:

function Input({ label, name, value, ...props }, { defaultTheme }) {
  const { theme, autoFocus, ...rootProps } = props
  return (
    <label
      htmlFor={name}
      children={label || defaultLabel}
      {...rootProps}
    >
    <input
      name={name}
      type="text"
      value={value || ``}
      theme={theme || defaultTheme}
      {...props}
    />
  )}
Input.contextTypes = {defaultTheme: React.PropTypes.object};

無狀態元件不像上述兩種方法在呼叫時會建立新例項,它建立時始終保持了一個例項,避免了不必要的檢查和記憶體分配,做到了內部優化。

無狀態元件不支援 “ref”

高階元件

高階元件通過函式和閉包,改變已有元件的行為,本質上就是 Decorator 模式在 React 的一種實現。

當寫著寫著無狀態元件的時候,有一天忽然發現需要狀態處理了,那麼無需徹底返工:)
往往我們需要狀態的時候,這個需求是可以重用的。

高階元件加無狀態元件,則大大增強了整個程式碼的可測試性和可維護性。同時不斷“誘使”我們寫出組合性更好的程式碼。

高階函式

function welcome() {
    let username = localStorage.getItem(`username`);
    console.log(`welcome ` + username);
}

function goodbey() {
    let username = localStorage.getItem(`username`);
    console.log(`goodbey ` + username);
}

welcome();
goodbey();

我們發現兩個函式有一句程式碼是一樣的,這叫冗餘唉。(平時可能會有一大段程式碼的冗餘)。

下面我們要寫一箇中間函式,讀取username,他來負責把username傳遞給兩個函式。

function welcome(username) {
    console.log(`welcome ` + username);
}

function goodbey(username) {
    console.log(`goodbey ` + username);
}

function wrapWithUsername(wrappedFunc) {
    let newFunc = () => {
        let username = localStorage.getItem(`username`);
        wrappedFunc(username);
    };
    return newFunc;
}

welcome = wrapWithUsername(welcome);
goodbey = wrapWithUsername(goodbey);

welcome();
goodbey();

好了,我們裡面的 wrapWithUsername 函式就是一個“高階函式”。
他做了什麼?他幫我們處理了 username,傳遞給目標函式。我們呼叫最終的函式 welcome的時候,根本不用關心 username是怎麼來的。

舉一反三的高階元件

下面是兩個冗餘的元件。

import React, {Component} from `react`

class Welcome extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ``
        }
    }

    componentWillMount() {
        let username = localStorage.getItem(`username`);
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>welcome {this.state.username}</div>
        )
    }
}

export default Welcome;
import React, {Component} from `react`

class Goodbye extends Component {
    constructor(props) {
        super(props);
        this.state = {
            username: ``
        }
    }

    componentWillMount() {
        let username = localStorage.getItem(`username`);
        this.setState({
            username: username
        })
    }

    render() {
        return (
            <div>goodbye {this.state.username}</div>
        )
    }
}

export default Goodbye;

我們可以通過剛剛高階函式的思想來建立一箇中間元件,也就是我們說的高階元件。

import React, {Component} from `react`

export default (WrappedComponent) => {
    class NewComponent extends Component {
        constructor() {
            super();
            this.state = {
                username: ``
            }
        }

        componentWillMount() {
            let username = localStorage.getItem(`username`);
            this.setState({
                username: username
            })
        }

        render() {
            return <WrappedComponent username={this.state.username}/>
        }
    }

    return NewComponent
}
import React, {Component} from `react`;
import wrapWithUsername from `wrapWithUsername`;

class Welcome extends Component {

    render() {
        return (
            <div>welcome {this.props.username}</div>
        )
    }
}

Welcome = wrapWithUsername(Welcome);

export default Welcome;
import React, {Component} from `react`;
import wrapWithUsername from `wrapWithUsername`;

class Goodbye extends Component {

    render() {
        return (
            <div>goodbye {this.props.username}</div>
        )
    }
}

Goodbye = wrapWithUsername(Goodbye);

export default Goodbye;

看到沒有,高階元件就是把 username 通過 props 傳遞給目標元件了。目標元件只管從 props裡面拿來用就好了。

為了程式碼的複用性,我們應該儘量減少程式碼的冗餘。

  1. 提取共享的state,如果有兩個元件都需要載入同樣的資料,那麼他們會有相同的 componentDidMount 函式。
  2. 找出重複的程式碼,每個元件中constructor 和 componentDidMount都幹著同樣的事情,另外,在資料拉取時都會顯示Loading… 文案,那麼我們應該思考如何使用高階元件來提取這些方法。
  3. 遷移重複的程式碼到高階元件
  4. 包裹元件,並且使用props替換state
  5. 儘可能地簡化

元件開發基本思想

單功能原則

使用react時,元件或容器的程式碼在根本上必須只負責一塊UI功能。

讓元件保持簡單

  • 如果元件根本不需要狀態,那麼就使用函式定義的無狀態元件。
  • 從效能上來說,函式定義的無狀態元件 > ES6 class 定義的元件 > 通過 React.createClass() 定義的元件。
  • 僅傳遞元件所需要的屬性。只有當屬性列表太長時,才使用{...this.props}進行傳遞。
  • 如果元件裡面有太多的判斷邏輯(if-else語句)通常意味著這個元件需要被拆分成更細的元件或模組。
  • 使用明確的命名能夠讓開發者明白它的功能,有助於元件複用。

基本準則

  • shouldComponentUpdate中避免不必要的檢查.
  • 儘量使用不可變資料型別(Immutable).
  • 編寫針對產品環境的打包配置(Production Build).
  • 通過Chrome Timeline來記錄元件所耗費的資源.
  • componentWillMount或者componentDidMount裡面通過setTimeOut或者requestAnimationFram來延遲執行那些需要大量計算的任務.

元件開發技巧

form表單裡的受控元件和不受控元件

受控元件

在大多數情況下,我們推薦使用受控元件來實現表單。在受控元件中,表單資料由 React 元件負責處理。下面是一個典型的受控組建。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ``};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert(`A name was submitted: ` + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

設定表單元素的value屬性之後,其顯示值將由this.state.value決定,以滿足React狀態的同一資料理念。每次鍵盤敲擊之後會執行handleChange方法以更新React狀態,顯示值也將隨著使用者的輸入改變。

對於受控元件來說,每一次 state(狀態)變化都會伴有相關聯的處理函式。這使得可以直接修改或驗證使用者的輸入和提交表單。

不受控元件

因為不受控元件的資料來源是 DOM 元素,當使用不受控元件時很容易實現 React 程式碼與非 React 程式碼的整合。如果你希望的是快速開發、不要求程式碼質量,不受控元件可以一定程度上減少程式碼量。否則。你應該使用受控元件。

一般情況下不受控元件我們使用ref來獲取DOM元素進行操作。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(event) {
    alert(`A name was submitted: ` + this.input.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={(input) => this.input = input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

元件條件判斷

三元函式元件判斷渲染

const sampleComponent = () => {
  return isTrue ? <p>True!</p> : <p>false!</p>
};

使用&&表示式替換不必要的三元函式

const sampleComponent = () => {
  return isTrue ? <p>True!</p> : <none/>
};
const sampleComponent = () => {
  return isTrue && <p>True!</p>
};

需要注意的是如果isTrue 為 0 ,其實會轉換成 false,但是在頁面中顯示的時候,&&還是會返回0顯示到頁面中。

多重巢狀判斷

// 問題程式碼
const sampleComponent = () => {
  return (
    <div>
      {flag && flag2 && !flag3
        ? flag4
        ? <p>Blah</p>
        : flag5
        ? <p>Meh</p>
        : <p>Herp</p>
        : <p>Derp</p>
      }
    </div>
  )
};

解決方案:

  • 最佳方案: 將邏輯移到子元件內部
  • 使用IIFE(Immediately-Invoked Function Expression 立即執行函式)
  • 滿足條件的時候使用return強制跳出函式
const sampleComponent = () => {
  const basicCondition = flag && flag2 && !flag3;
  if (!basicCondition) return <p>Derp</p>;
  if (flag4) return <p>Blah</p>;
  if (flag5) return <p>Meh</p>;
  return <p>Herp</p>
}

setState非同步性

在某些情況下,React框架出於效能優化考慮,可能會將多次state更新合併成一次更新。正因為如此,setState實際上是一個非同步的函式。 如果在呼叫setState()函式之後嘗試去訪問this.state,你得到的可能還是setState()函式執行之前的結果。

但是,有一些行為也會阻止React框架本身對於多次state更新的合併,從而讓state的更新變得同步化。 比如: eventListeners, Ajax, setTimeout 等等。

React框架之所以在選擇在呼叫setState函式之後立即更新state而不是採用框架預設的方式,即合併多次state更新為一次更新,是因為這些函式呼叫(fetch,setTimeout等瀏覽器層面的API呼叫)並不處於React框架的上下文中,React沒有辦法對其進行控制。React在此時採用的策略就是及時更新,確保在這些函式執行之後的其他程式碼能拿到正確的資料(即更新過的state)。

解決setState函式非同步的辦法?

根據React官方文件,setState函式實際上接收兩個引數,其中第二個引數型別是一個函式,作為setState函式執行後的回撥。通過傳入回撥函式的方式,React可以保證傳入的回撥函式一定是在setState成功更新this.state之後再執行。

this.setState({count: 1}, () => {
    console.log(this.state.count); // 1
})

React原始碼中setState的實現

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === `object` ||
    typeof partialState === `function` ||
    partialState == null,
    `setState(...): takes an object of state variables to update or a ` +
    `function which returns an object of state variables.`
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, `setState`);
  }
};

updater的這兩個方法,和React底層的Virtual Dom(虛擬DOM樹)的diff演算法有緊密的關係,所以真正決定同步還是非同步的其實是Virtual DOMdiff演算法。

依賴注入

React中,想做依賴注入(Dependency Injection)其實相當簡單。可以通過props來進行傳遞。但是,如果元件數量很多,並且元件巢狀層次很深的話,這種方式就不太合適。

高階元件

// inject.jsx
var title = `React Dependency Injection`;
export default function inject(Component) {
  return class Injector extends React.Component {
    render() {
      return (
        <Component
          {...this.state}
          {...this.props}
          title={ title }
        />
      )
    }
  };
}
// Title.jsx
export default function Title(props) {
  return <h1>{ props.title }</h1>;
}
// Header.jsx
import inject from `./inject.jsx`;
import Title from `./Title.jsx`;

var EnhancedTitle = inject(Title);
export default function Header() {
  return (
    <header>
      <EnhancedTitle />
    </header>
  );
}

context

React v16.3.0 之前的 Context:

var context = { title: `React in patterns` };
class App extends React.Component {
  getChildContext() {
    return context;
  }
  // ...
}

App.childContextTypes = {
  title: PropTypes.string
};
class Inject extends React.Component {
  render() {
    var title = this.context.title;
  // ...
  }
}
Inject.contextTypes = {
  title: PropTypes.string
};

之前的 Context 作為一個實驗性質的 API,直到 React v16.3.0 版本前都一直不被官方所提倡去使用,其主要原因就是因為在子元件中使用 Context 會破壞 React 應用的分型架構。

這裡的分形架構指的是從理想的 React 應用的根元件樹中抽取的任意一部分都仍是一個可以直接執行的子元件樹。在這個子元件樹之上再包一層,就可以將它無縫地移植到任意一個其他的根元件樹中。

但如果根元件樹中有任意一個元件使用了支援透傳的 Context API,那麼如果把包含了這個元件的子元件樹單獨拿出來,因為缺少了提供 Context 值的根元件樹,這時的這個子元件樹是無法直接執行的。

並且他有一個致命缺陷:任何一箇中間傳遞的元件shouldComponentUpdate 函式返回false,元件都不會得到更新。

新的Context Api

新的Context Api 採用宣告式的寫法,並且可以透過shouldComponentUpdate 函式返回false的元件繼續向下傳播,以保證目標元件一定可以接收到頂層元件 Context 值的更新,一舉解決了現有 Context API 的兩大弊端,也終於成為了 React 中的第一級(first-class) API

新的 Context API 分為三個組成部分:

  1. React.createContext 用於初始化一個 Context
  2. XXXContext.Provider作為頂層元件接收一個名為 valueprop,可以接收任意需要被放入 Context 中的字串,數字,甚至是函式。
  3. XXXContext.Consumer作為目標元件可以出現在元件樹的任意位置(在 Provider 之後),接收 children prop,這裡的 children 必須是一個函式(context =&gt; ())用來接收從頂層傳來的 Context
const ThemeContext = React.createContext(`light`);

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

事件處理中的this指向問題

class Switcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: `React in patterns` };
  }
  render() {
    return (
      <button onClick={ this._handleButtonClick }>
        click me
      </button>
    );
  }

  _handleButtonClick() {
    console.log(`Button is clicked inside ${ this.state.name }`);
    // 將導致
    // Uncaught TypeError: Cannot read property `state` of null
  }
}

我們可以通過下面三種方式簡單實現this指向的繫結:

  • constructor 中事先繫結 this._buttonClick = this._handleButtonClick.bind(this);
  • 呼叫時使用箭頭函式 <button onClick={ () => this._buttonClick() }>
  • ES7中的繫結操作符 <button onClick={ ::this._buttonClick() }>

給setState傳入回撥函式

setState() 不僅能接受一個物件,還能接受一個函式作為引數呢,該函式接受該元件前一刻的 state 以及當前的 props 作為引數,計算和返回下一刻的 state。

// assuming this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// this.state.count === 1, not 3

this.setState((prevState, props) => ({
  count: prevState.count + props.increment
}));
// Passing object
this.setState({ expanded: !this.state.expanded });

// Passing function
this.setState(prevState => ({ expanded: !prevState.expanded }));

元件切換技巧

import HomePage from `./HomePage.jsx`;
import AboutPage from `./AboutPage.jsx`;
import UserPage from `./UserPage.jsx`;
import FourOhFourPage from `./FourOhFourPage.jsx`;

const PAGES = {
  home: HomePage,
  about: AboutPage,
  user: UserPage
};

const Page = (props) => {
  const Handler = PAGES[props.page] || FourOhFourPage;

  return <Handler {...props} />
};

React style

元件分類

基礎元件, 佈局元件, 排版元件

給無狀態的純UI元件應用樣式

請保持樣式遠離那些離不開state的元件. 比如路由, 檢視, 容器, 表單, 佈局等等不應該有任何的樣式或者css class出現在元件上. 相反, 這些複雜的業務元件應該有一些帶有基本功能的無狀態UI元件組成.

class SampleComponent extends Component {
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <Heading children=`Sign In`/>
        <Input
          name=`username`
          value={username}
          onChange={this.handleChange}/>
        <Input
          type=`password`
          name=`password`
          value={password}
          onChange={this.handleChange}/>
        <Button
          type=`submit`
          children=`Sign In`/>
      </form>
    )
  }
}

// 表達元件(帶樣式)
const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: `inherit`,
    fontSize: `inherit`,
    fontWeight: `bold`,
    textDecoration: `none`,
    display: `inline-block`,
    margin: 0,
    paddingTop: 8,
    paddingBottom: 8,
    paddingLeft: 16,
    paddingRight: 16,
    border: 0,
    color: `white`,
    backgroundColor: `blue`,
    WebkitAppearance: `none`,
    MozAppearance: `none`
  }

  return (
    <button {...props} style={sx}/>
  )
}

樣式模組(style module)

一般來說, 在元件內寫死(hard code)樣式應該是要被避免的. 這些有可能被不同的UI元件分享的樣式應該被分開放入對應的模組中.

// 樣式模組
export const white = `#fff`;
export const black = `#111`;
export const blue = `#07c`;

export const colors = {
  white,
  black,
  blue
};

export const space = [
  0,
  8,
  16,
  32,
  64
];

const styles = {
  bold: 600,
  space,
  colors
};

export default styles
// button.jsx
import React from `react`
import { bold, space, colors } from `./styles`

const Button = ({
  ...props
  }) => {
  const sx = {
    fontFamily: `inherit`,
    fontSize: `inherit`,
    fontWeight: bold,
    textDecoration: `none`,
    display: `inline-block`,
    margin: 0,
    paddingTop: space[1],
    paddingBottom: space[1],
    paddingLeft: space[2],
    paddingRight: space[2],
    border: 0,
    color: colors.white,
    backgroundColor: colors.blue,
    WebkitAppearance: `none`,
    MozAppearance: `none`
  };

  return (
    <button {...props} style={sx}/>
  )
};

樣式函式(Style Functions)

// Modular powers of two scale
const scale = [
  0,
  8,
  16,
  32,
  64
];

// 通過這個函式去取得一部分的樣式
const createScaledPropertyGetter = (scale) => (prop) => (x) => {
  return (typeof x === `number` && typeof scale[x] === `number`)
    ? {[prop]: scale[x]}
    : null
};
const getScaledProperty = createScaledPropertyGetter(scale);

export const getMargin = getScaledProperty(`margin`);
export const getPadding = getScaledProperty(`padding`);
// 樣式函式的用法
const Box = ({
  m,
  p,
  ...props
  }) => {
  const sx = {
    ...getMargin(m),
    ...getPadding(p)
  };

  return <div {...props} style={sx}/>
};

// 元件用法.
const Box = () => (
  <div>
    <Box m={2} p={3}>
      A box with 16px margin and 32px padding
    </Box>
  </div>
);

常見小坑

state不更新?

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false,
      inputVal: props.inputValue
    };
  }

  render() {
    return <div>{this.state.inputVal && <AnotherComponent/>}</div>
  }
}

這樣做的危險在於, 有可能元件的props發生了改變但是元件卻沒有被更新. 新的props的值不會被React認為是更新的資料因為構造器constructor或者getInitialState方法在元件建立之後不會再次被呼叫了,因此元件的state不再會被更新。 要記住, State的初始化只會在元件第一次初始化的時候發生。

class SampleComponent extends Component {
  // constructor function (or getInitialState)
  constructor(props) {
    super(props);
    this.state = {
      flag: false
    };
  }

  render() {
    return <div>{this.props.inputValue && <AnotherComponent/>}</div>
  }
}

更乾淨的render函式?

更乾淨的render函式? 這個概念可能會有點讓人疑惑.

其實在這裡乾淨是指我們在shouldComponentUpdate這個生命週期函式裡面去做淺比較, 從而避免不必要的渲染.

class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || []}/>
        )}
      </div>
    );
  }
}

這種寫法的問題在於{this.props.options || []} 這種寫法會導致所有的Cell都被重新渲染即使只有一個cell發生了改變. 為什麼會發生這種事呢?

仔細觀察你會發現, options這個陣列被傳到了Cell這個元件上, 一般情況下, 這不會導致什麼問題. 因為如果有其他的Cell元件, 元件會在有props發生改變的時候淺對比props並且跳過渲染(因為對於其他Cell元件, props並沒有發生改變). 但是在這個例子裡面, 當optionsnull時, 一個預設的空陣列就會被當成Props傳到元件裡面去. 事實上每次傳入的[]都相當於建立了新的Array例項. 在JavaScript裡面, 不同的例項是有不同的實體的, 所以淺比較在這種情況下總是會返回false, 然後元件就會被重新渲染. 因為兩個實體不是同一個實體. 這就完全破壞了React對於我們元件渲染的優化.

const defaultval = [];  // <---  也可以使用defaultProps
class Table extends PureComponent {
  render() {
    return (
      <div>
        {this.props.items.map(i =>
          <Cell data={i} options={this.props.options || defaultval}/>
        )}
      </div>
    );
  }
}

還是多次重新渲染

class App extends PureComponent {
  render() {
    return <MyInput
      onChange={e => this.props.update(e.target.value)}/>;
  }
}
class App extends PureComponent {
  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update.bind(this)}/>;
  }
}

在上面的兩個壞實踐中, 每次我們都會去建立一個新的函式實體. 和第一個例子類似, 新的函式實體會讓我們的淺比較返回false, 導致元件被重新渲染. 所以我們需要在更早的時候去bind我們的函式.

class App extends PureComponent {
  constructor(props) {
    super(props);
    this.update = this.update.bind(this);
  }

  update(e) {
    this.props.update(e.target.value);
  }

  render() {
    return <MyInput onChange={this.update}/>;
  }
}

命名

引用命名

React模組名使用帕斯卡命名,例項使用駱駝式命名

// bad
import reservationCard from `./ReservationCard`;

// good
import ReservationCard from `./ReservationCard`;

// bad
const ReservationItem = <ReservationCard />;

// good
const reservationItem = <ReservationCard />;

高階模組命名

// bad
export default function withFoo(WrappedComponent) {
  return function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }
}

// good
export default function withFoo(WrappedComponent) {
  function WithFoo(props) {
    return <WrappedComponent {...props} foo />;
  }

  const wrappedComponentName = WrappedComponent.displayName
    || WrappedComponent.name
    || `Component`;

  WithFoo.displayName = `withFoo(${wrappedComponentName})`;
  return WithFoo;
}

屬性命名

避免使用DOM相關的屬性來用作其他的用途。

// bad
<MyComponent style="fancy" />

// good
<MyComponent variant="fancy" />

私有函式新增 _ 字首?

在React模組中,不要給所謂的私有函式新增 _ 字首,本質上它並不是私有的。

為什麼?_ 下劃線字首在某些語言中通常被用來表示私有變數或者函式。但是不像其他的一些語言,在JS中沒有原生支援所謂的私有變數,所有的變數函式都是共有的。儘管你的意圖是使它私有化,在之前加上下劃線並不會使這些變數私有化,並且所有的屬性(包括有下劃線字首及沒有字首的)都應該被視為是共有的。

Ordering React 模組生命週期

class extends React.Component 的生命週期函式:
可選的 static 方法

  • constructor 建構函式
  • getChildContext 獲取子元素內容
  • componentWillMount 模組渲染前
  • componentDidMount 模組渲染後
  • componentWillReceiveProps 模組將接受新的資料
  • shouldComponentUpdate 判斷模組需不需要重新渲染
  • componentWillUpdate 上面的方法返回 true, 模組將重新渲染
  • componentDidUpdate 模組渲染結束
  • componentWillUnmount 模組將從DOM中清除, 做一些清理任務

點選回撥或者事件處理器 如 onClickSubmit()onChangeDescription()

render 裡的 getter 方法 如 getSelectReason()getFooterContent()

可選的 render 方法 如 renderNavigation()renderProfilePicture()

render render() 方法

如何定義 propTypes, defaultProps, contextTypes, 等等其他屬性…

import React from `react`;
import PropTypes from `prop-types`;

const propTypes = {
  id: PropTypes.number.isRequired,
  url: PropTypes.string.isRequired,
  text: PropTypes.string,
};

const defaultProps = {
  text: `Hello World`,
};

class Link extends React.Component {
  static methodsAreOk() {
    return true;
  }

  render() {
    return <a href={this.props.url} data-id={this.props.id}>{this.props.text}</a>;
  }
}

Link.propTypes = propTypes;
Link.defaultProps = defaultProps;

export default Link;

相關文章