Posts match “ ReactJS ” tag:

入門

開始學習了解 React 的方式就是使用 JSFiddle 來觀察實作的範例: Hello Worlds。

下載入門套件

下載入門範例與套件。

解開壓縮檔後,在根目錄建立一個 helloworld.html 然後輸入下面的例子。

<!DOCTYPE html>
<html>
  <head>
    <script src="build/react.js"></script>
    <script src="build/JSXTransformer.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/jsx">
      /** @jsx React.DOM */
      React.renderComponent(
        <h1>Hello, world!</h1>,
        document.getElementById('example')
      );
    </script>
  </body>
</html>

在 Javascript 裡面使用 XML 格式的語法叫做 JSX,這種語法是官方推薦的寫法。可以參考學習更多關於 JSX 的用法。為了使 JSX 可以正確的轉換為 Javascript 我們會使用 <script type='text/jsx'> 標簽,以及記得要載入 JSXTransformer.js 以確保程式正確執行。在這邊我們會先提醒那些有程式開發經驗的學習者。
不要忘記在一開始加入 /** @jsx React.DOM */,這不是一般註解,它是 React 用來定義要處理的 JSX 。如果你沒有加入這個片段,你的程式碼將不會被轉換。

獨立的檔案

你的 React JSX 檔案可以是分開的獨立檔案。接著讓我們建立 src/helloworld.js

/** @jsx React.DOM */
React.renderComponent(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

然後在 helloworld.html 加入

<script type="text/jsx" src="src/helloworld.js"></script>

即使是分開檔案功能仍然可以運行,這在大型專案中將有助于 DRY 原則,同樣功能的元件應該抽離獨立。

離線轉換 JSX

安裝這個指令轉換工具(command-line)需要先安裝 npm

npm install -g react-tools

然後轉換 src/helloworld.js 檔案為原生 Javascript。

jsx --watch src/ build/

看看自動產生的 build/helloworld.js 如下,因為使用了 --watch 參數,你可以直接修改 JSX ,然後工具就會自動更新。指令的語法是 jsx --watch <source directory> <output directory> ,所以請不要指定到檔案。

/** @jsx React.DOM */
React.renderComponent(
  React.DOM.h1(null, 'Hello, world!'),
  document.getElementById('example')
);

注意
註解的解析器是非常嚴格的;為了能夠提取 @jsx 修飾子,兩件事情必須遵守:

  1. @jsx 註解區塊必須要在檔案或程式碼的開頭,也必須是第一段註解。
  2. 註解的開頭必須是 /**/*// 將會不正常)。 如果解析器找不到 @jsx註解區塊 輸出時就不會執行轉換。

讓我們接著更新 HTML 如下:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello React!</title>
    <script src="build/react.js"></script>
    <!-- No need for JSXTransformer! -->
  </head>
  <body>
    <div id="example"></div>
    <script src="build/helloworld.js"></script>
  </body>
</html>

注意: type='text/jsx' 要拿掉,否則無法運作。

使用 CommonJS

如果你想要使用模組化的 React , 請 fork 官方專案,然後執行 npm installgrunt,便可以產出遵循 CommonJS 規則模組化的程式碼。官方提供的 jsx 編譯工具可以整合可以簡單地整合到大部份的封裝系統。
CommonJS 補充

下一步

查閱官方教學和其他/examples範例目錄學習更多。

學習手冊

本篇教學會協助你建立一個簡單,但是實用的留言框功能,你可以放置到你的 blog 中。類似于DisqusLiveFyre,或者 Facebook comments。
留言框提供下列功能:

  • 留言框的界面(view)。
  • 一個表單(form)可以送出留言。
  • 為你的後端程式提供一個 Hooks ,

Hooks 簡易說明:Hooks 英文翻譯為鉤子,在程式術語中所表達的是在程式特定位置埋入一段預留的程式碼,用來呼叫其他對應的程式碼。可以大略想成在某個片段先空出一個位置,這個位置可以在事後再放入動作,不放也沒關係。

同時也有下列的特點:

  • 優化留言:留言在儲存到伺服器之前就出現在列表中,這會讓使用者有變快的感覺。
  • 及時更新:當其他使用者留言時,我們將及時的取出他們的留言並放置到界面中,不用等使用者自己更新頁面。
  • Markdown 格式:使用者可以用 Markdown 格式來留言,這可使得留言的排版更多元整齊。

直接閱覽原始碼

起步

在這一篇教學中我們會使用預先建置好放置在 CDN 的 Javascript 檔案。開啟你最愛的文字編輯器例如:Sublime text 然後建立一個 HTML 文件如下:

<!-- template.html -->
<html>
  <head>
    <title>Hello React</title>
    <script src="http://fb.me/react-0.8.0.js"></script>
    <script src="http://fb.me/JSXTransformer-0.8.0.js"></script>
    <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/jsx">
      /**
       * @jsx React.DOM
       */
      // The above declaration must remain intact at the top of the script.

      // 上面的宣告註解仍然得維持在 <script> 標簽中的頂部。

      // Your code here!您的程式碼。

    </script>
  </body>
</html>

為了完成教學剩下的部分,我們開始在 <script> 標簽內撰寫 Javascript。

第一個元件

整個 React 本身就是在模組化和設計可組成的元件,例如這個留言框的範例接下來就會遵循元件的架構。

- CommentBox
  - CommentList
    - Comment
  - CommentForm

在使用 React 開發時一開始定義好整個架構可以協助您更快速準確的開發。
讓我們來建立 CommentBox 留言框這個元件,它只需要一個簡單的 <div>

// tutorial1.js

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
React.renderComponent(
  <CommentBox />,
  document.getElementById('content')
);

小提醒:在 React 中使用 JSX <div><CommentBox /> 的標簽時 / 結尾的關閉標簽一定要加,否則會造成錯誤。

JSX 語法

首先就是你應該注意到那些在 Javascript 中類似 XML 的語法,這些特殊的 JSX 語法是為了讓我們更方便直覺的維護程式碼的糖衣語法,我們會需要使用預先編譯器負責去轉換這些糖衣語法為原生的 Javascript。

糖衣語法:指程式語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。

事實上,如果不使用 JSX 你依然可以撰寫 React 。不過得改用原生的 React 物件去組織程式碼,而寫出來的程式碼和透過 JSXTransformer 轉換的語法基本上是會一樣的。如下範例:

var CommentBox = React.createClass({
  render: function () {
    reutrn (
      React.DOM.div({
        className: 'commentBox',
        children: 'Hello, world! I am a CommentBox.'
      })
    );
  }
});

React.renderComponent(
  CommentBox({}),
  document.getElementById('content')
)

小提醒:開發時我們會使用 JSXTransformer 以方便開發,而當要部署 Production 的時候,建議要先行編譯轉換以提升效能。

JSX 不是一定要使用的,不過官方建議 JSX 的語法比純 Javascript 更簡潔易維護,如果你想了解更多可以閱讀 JSX Syntax

剛剛我們做了什麼?

React.createClass() 透過傳入一個 Javascript 物件包含一些方法(method)就可以建立新的 React 元件物件。在所有方法裡面最重要的就是 render() ,它會傳回一個 React 元件的樹狀結構,最後被輸出成 HTML 。
在 JSX 裡面的 <div> 標簽並不是實際的 DOM 元素。他只是 React div 元件的實例物件(React.DOM.div)。
你可以把它當成一種標記或者資料片段,他的功用只是讓 React 知道該怎麼處理這些標記,進而產生對應的 HTML。
這也是 React 本身提供的一套安全機制。它並不會直接產生 HTML 字串,它是透過解析標記最後透過內部一套機制產生 DOM 物件 ,所以預設就可以防止關於 XSS 攻擊。
因此你不需要撰寫一段標準完整的 HTML 語法,只要傳回一個你或其他人寫的元件結構。這就是 React 所謂『可組成』的概念,前端發展可維護性程式碼的核心概念之一。
React.renderComponent() 這個方法是用來將根元件實例化,並且把 React 產生的標記注入到第二個參數指向的原始 HTML DOM 元素。以上例來說就是第一個參數是先初始化 <CommentBox /> 把它 new 出來後,再把 React 解析產生的 HTML (嚴格說是 DOM 元素)注入到 document.getElementById('content') 這個元素中。

組成元件

讓我們來繼續建構 CommentListCommentForm:

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

接著修改 CommentBox 元件,使用我們剛剛完成的 CommentListCommentForm

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

注意到我們在上面的範例混合了 HTML 的 <h1> 標簽和 <CommentList> 等元件,HTML 標簽是正規的 React 元件,就跟你定義的元件一樣,但這之中有一點點不同。 JSX 的編譯器會自動把 HTML 標簽轉成 React.DOM.tagName ,這是為了防止污染全域的命名空間。

補充:如果你對於不使用 JSX 的寫法比較有興趣下面列出一段大概的用法:

var CommentBox = React.createClass({displayName: 'CommentBox',
  render: function () {
    return (
      React.DOM.div( {className:"commentBox"},
        React.DOM.h1(null, "Comments"),
        CommentList(null ),
        CommentForm(null )
      )
    );
  }
});

元件屬性(Component Properties)

建立 Comment 元件。我們想要顯示留言者的名字和訊息,並且重複使用相同的程式碼給每一則獨立的留言。讓我們加入一些留言到 CommentList

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

現在我們使用類似 XML 的語法格式在 CommentList 放入一些子元素 Comment 和一些資料到屬性。每一個 <Comment> 表示一則留言訊息。
在結構上資料是透過父元素傳給子元素的,要完成把資料傳給子元素有一個重要的方式叫做 props 就是 Properties 的縮寫,或者說可以透過 props 取得父元素的資料。

小筆記:邏輯上 CommentList 是整個留言列表所以資料會繫結到這個元件上,再透過迭代的方式去產生底下的 Comment 元件。上面的範例我們先使用 hardcode 的方式讓讀者理解概念。

使用 props

建立 Comment 元件類別,它將會從 CommentList 讀取資料然後渲染標記。再複習一次整個 React 的架構就是分別建立各個元件,然後透過組合的方式來實現 reuse 的原則,設計上我們通常會讓父元素取得資料,這樣就可以在內部直接迭代渲染輸出 HTML 。
而在取得資料的部分,通常我們會使用 React 提供的 props 來實現 。如下面範例:

var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

在 JSX 中透過大括號,你可以在裡面使用 Javascript 表示式(例如取得任何一個屬性或子元素)。你現在可以放入一些文字或 React 元件到這個結構中。然後我們可以透過屬性的名稱去存取資料,使用的方式是利用 this.props 或在這個巢狀結構下的任何元素 ex: this.props.children

加入 Markdown 功能

Markdown 是一種簡單的文字格式。舉個例子用 * 包圍的文字會變成斜體, HTML 中為強調語氣的語意。要增加這個功能我們可以使用第三方的函式庫 Showdown。透過這個函式庫可以把 Markdown 格式的文字轉換成 HTML。下面我們直接使用 CDN 上的檔案載入此函式庫。

<!-- template.html -->
<head>
  <title>Hello React</title>
  <script src="http://fb.me/react-0.8.0.js"></script>
  <script src="http://fb.me/JSXTransformer-0.8.0.js"></script>
  <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
</head>

接著就可以在程式碼中使用它

var converter = new Showdown.converter();
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {converter.makeHtml(this.props.children.toString())}
      </div>
    );
  }
});

這邊我們呼叫了 Showdown 的函式庫,我們需要轉換 this.props.children ,讓他從 React 的換行文字先轉成純文字,接著 Showdown 就可以轉換。在這個過程中,我們通常會明確的呼叫 toString()
但是這邊有一個問題,我們輸出的留言看起來會變成 <p>This is <em>another</em> comment</p>。我們希望這些標簽可以正確被的輸出成 HTML。
這是 React 預設防止 XSS 攻擊的機制。這邊提供一種方式,但是框架會警告你不要使用它:

var converter = new Showdown.converter();
var Comment = React.createClass({
  render: function() {
    var rawMarkup = converter.makeHtml(this.props.children.toString());
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={{__html: rawMarkup}} />
      </div>
    );
  }
});

這是一個特殊的 API 企圖讓寫入 HTML 變得比較困難一點,但因為 Showdown 需要輸出 HTML 的關係我們必須要開一個後門。

提醒:使用這個功能你必須依賴 Showdown 本身的安全性,以及確保被轉換的資料不會有風險。

連結資料模型

到目前為止,我們已經在程式碼直接寫入了一些留言,實務上我們的資料通常會從資料庫來,這邊我們用一些 JSON 格式的資料來模擬實際的狀況。

var data = [
  {author: "Pete Hunt", text: "This is one comment"},
  {author: "Jordan Walke", text: "This is *another* comment"}
];

在這邊我們會透過模組化的方式去取得資料,實務上舉例就是通常我們會取得一批資料傳給 CommentList 透過程式去一筆一筆輸出。避免使用 hardcode 的方式像上面的例子 <Comment>
修改 CommentBoxrenderComponent() ,換成使用 props 取得資料後傳給 CommentList。

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

React.renderComponent(
  <CommentBox data={data} />,
  document.getElementById('content')
);

現在資料已經是透過 JSON 格式來取得了並且和 CommentList 繫結,接著就讓我們動態的輸出這些留言吧。

小筆記:撰寫 React 的時候概念上我們可以把 createClass 就理解成是在建立一個類別,renderComponent() 就是在實例化物件,此時該傳入的資料(參數)我們就透過標簽屬性(attributes)帶入。

var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return <Comment author={comment.author}>{comment.text}</Comment>;
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

透過 map 函式去處理陣列,處理後的結果如下圖。


從下面編譯過的程式碼,我們可以知道 React 會自動去迭代輸出陣列。

var CommentList = React.createClass({displayName: 'CommentList',
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return Comment( {author:comment.author}, comment.text);
    });
    return (
      React.DOM.div( {className:"commentList"},
        commentNodes
      )
    );
  }
});

範例到此相信有經驗的開發者已經能夠掌握 React 的核心概念了。我們在從範例的角度說明到目前為止我們做了什麼:首先,我們建立了 CommentBox,在裡面部署了 CommentListCommentForm 物件,CommentList 透過 this.props.data 從父元素 CommentBox 那邊取得資料。這個屬性把資料繫結到此物件上。接著在 CommentList 類別裡面我們用得到的資料組合出 Comment 的陣列,並讓 Comment 這個元件去負責每一則留言的呈現。最後我們又用了 CommentForm 元件來實踐送出留言的功能。
對於一些比較少OOP經驗的開發者,模組化剛開始可能會覺得有點亂。筆者透過一張圖大略的說明整個基本的流程:

從伺服器取得資料

讓我們移除寫死的資料改用一些從伺服器端來的動態資料。在這一步我們會透過 url 來取得資料。

React.renderComponent(
  <CommentBox url="comments.json" />,
  document.getElementById('content')
);

這個元件將跟之前的不一樣,因為它必須要自己重新載入並輸出。一開始這個元件並沒有任何資料,直到發出的 Request 從伺服器取得資料,這個時候元件就需要重新渲染。在上面範例中我們只是單純地取得某個 JSON 檔案。

狀態回應

到目前為止每一個元件都根據自身的 props 取得的資料渲染了一次,props 本身是靜態不會變動的。它們從父元素取得,而且是父元素擁有的。為了完成互動功能,需要-元件的狀態屬性 this.state 。這個屬性本身是 private ,只能透過呼叫 this.setState() 去更改。當狀態改變的時候,元件就會重新渲染輸出。
render() 是用來處理關於 this.propsthis.state 的資料。我們會把資料放在 props 和 state 裡面接著透過 render() 去處理該如何呈現。整個框架必須確保所有資料和UI上呈現的是一致的。
當伺服器獲得資料,我們就需要把我們有的資料新增或修改到留言框上。接著讓我們加入一個留言資料的陣列到 CommentBox 的狀態屬性上。

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState() 這個方法再整個元件的生命週期中只會執行一次,目的是用來設定初始化 this.state 的資料。按上面的例子就是你傳回了一個 {data: []} 物件給 this.state 。之後便可以使用 this.state.data 取得這個陣列。

更新狀態

當元件第一次被建立的時候,我們想透過 GET 方式從伺服器取得一些 JSON 格式的資料,替 state 更新對應的最新資料。
在實務上這通常是動態從資料庫,API 或其他服務取得,不過在這個例子為了簡化我們使用靜態的 JSON 檔案。
在根目錄建立一個 comments.json 如下

[
  {"author": "Pete Hunt", "text": "This is one comment"},
  {"author": "Jordan Walke", "text": "This is *another* comment"}
]

我們會搭配使用 jQuery 來協助我們發出非同步請求給伺服器,以取得資料。
注意:因為加入 jQuery 這已經是一個 AJAX 應用程式,你會使用網頁伺服器來執行這個範例,而不是單純在瀏覽器執行檔案。最簡單的方式是在目錄下執行 python -m SimpleHTTPServer,或者熟悉 Grunt 的開發者可以使用grunt-init-simple-server

var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    $.ajax({
      url: 'comments.json',
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error("comments.json", status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

componentWillMount 方法會在元件渲染前自動被呼叫執行。在這個例子裡動態更新的關鍵是呼叫 this.setState()
我們把本來的陣列資料移除,取代用從伺服器取得資料的方式,這裡為了保持簡單是使用 comments.json
為了展示這種及時反應的效果,這邊加了一段程式碼達到輪詢的功能,意思是程式本身會不斷的重複去查詢 comments.json 的資料。 實務上你應該使用 secket.io 才不會造成效能很糟糕。

var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

React.renderComponent(
  <CommentBox url="comments.json" pollInterval={2000} />,
  document.getElementById('content')
);

我們把 AJAX 取得資料的片段獨立成一個方法,然後第一次載入的時候會呼叫一次,接著設定每兩秒執行一次。

新增留言

是時候來建置我們的留言表單了,這個留言表單元件應該要詢問使用者他的名字和留言訊息,接著提交給伺服器儲存留言。

var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

表單運作的流程是:當使用者提交表單時,我們應該清除裡面的資料,接著發出一個 Reauest 給伺服器,最後更新留言列表。

var CommentForm = React.createClass({
  handleSubmit: function() {
    var author = this.refs.author.getDOMNode().value.trim();
    var text = this.refs.text.getDOMNode().value.trim();
    if (!text || !author) {
      return false;
    }
    // TODO: send request to the server

    this.refs.author.getDOMNode().value = '';
    this.refs.text.getDOMNode().value = '';
    return false;
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input type="text" placeholder="Your name" ref="author" />
        <input
          type="text"
          placeholder="Say something..."
          ref="text"
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});
  • 事件:
    React 在元件上繫結事件是用 camelCase 命名規則,當表單驗證過並提交時我們使用了一個 onSubmit 事件來處理。這裡我們永遠回傳一個 false 來阻止瀏覽器預設的提交行為。(如果你比較喜歡透過 event 參數使用 e.preventDefault(),你也可以用它來取代 return false

  • Refs:
    使用 ref 屬性只設定子元素的名稱,就可以透過 this.refs 參考到該元件。我們可以呼叫 getDOMNode() 來取得原生的 DOM 元素。

  • 透過 props 使用 callback
    當使用者送出一則留言,我們將需要更新留言列表加入新的訊息, 在 CommentBox 實作這些邏輯是比較合理的,因為 CommentBox 負責掌控整個元件的狀態 this.state
    所以在這個範例裡,我們需要從子元素把資料傳給父元素 CommentBox ,讓 CommentBox 去更新狀態,為了完成這個目的,我們在 props 放入一個 callback 函式,如此一來子元素便能透過呼叫這個函式把資料帶給父元素。
    看到這邊可能會有些混亂,沒關係讓我們先直接看 CommentBox 的程式碼:

var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    // TODO: submit to the server and refresh the list

  },
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm
          onCommentSubmit={this.handleCommentSubmit}
        />
      </div>
    );
  }
});

然後在 CommentForm 呼叫

var CommentForm = React.createClass({
  handleSubmit: function() {
    var author = this.refs.author.getDOMNode().value.trim();
    var text = this.refs.text.getDOMNode().value.trim();
    this.props.onCommentSubmit({author: author, text: text});
    this.refs.author.getDOMNode().value = '';
    this.refs.text.getDOMNode().value = '';
    return false;
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input type="text" placeholder="Your name" ref="author" />
        <input
          type="text"
          placeholder="Say something..."
          ref="text"
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

總結上面敘述就是我們應該把處理寫入留言(提交到伺服器)的邏輯和程式碼寫在 CommentBox 元件裡面,就是 handleCommentSubmit 這個方法,接著透過 <CommentForm onCommentSubmit={this.handleCommentSubmit} /> 的方式把方法傳給子元件 CommentFormCommentForm 有需要送出留言的時候就可以透過 this.props.onCommentSubmit() 去呼叫。資料統一都由父元素管理是比較合理的做法。到目前為止我們概略的了解關於狀態和資料還有處理的方法應該放在父元素,你可以在父元素裡面在放置其他元件,如果子元素需要動用父元素的功能或資料就透過屬性(attributes)帶入參數的方式傳進去。

現在讓我們來完成 callback 函式該執行的任務

var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm
          onCommentSubmit={this.handleCommentSubmit}
        />
      </div>
    );
  }
});

優化UI更新

這個範例到這邊已經全部完成了(實際呼叫 AJAX 後端操作並沒有實作在範例裡),但是感覺有點慢,因為我們必須要等待發出的 Request 完成處理留言才會出現。我們可以優化這個部分讓使用者感覺更快。

var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    var comments = this.state.data;
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm
          onCommentSubmit={this.handleCommentSubmit}
        />
      </div>
    );
  }
});

上面這段程式碼的重點在加入

var comments = this.state.data;
var newComments = comments.concat([comment]);
this.setState({data: newComments});

透過這段程式使用者的留言在送出時本地端就先更新了,再發出 Request 給伺服器。後續元件本身會自己在去跟伺服器讀取最新的資訊。

恭喜

你已經完成建置這個留言框的功能,也初步對如何使用 React 有了認識,學習更多關於為什麼使用 React 或查閱 API 參考

最後附上完成的範例程式碼:

<!-- template.html -->
<html>
  <head>
    <title>Hello React</title>
    <script src="http://fb.me/react-0.8.0.js"></script>
    <script src="http://fb.me/JSXTransformer-0.8.0.js"></script>
    <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/jsx">
      /**
       * @jsx React.DOM
       */
      var converter = new Showdown.converter();
      var data = [
        {author: "Pete Hunt", text: "This is one comment"},
        {author: "Jordan Walke", text: "This is *another* comment"}
      ];
      var CommentBox = React.createClass({
        loadCommentsFromServer: function () {
          $.ajax({
            url: this.props.url,
            dataType: 'json',
            success: function (data) {
              this.setState({data: data});
            }.bind(this),
            error: function (xhr, status, err) {
              console.error('comments.json', status, err.toString());
            }.bind(this)
          });
        },
        handleCommentSubmit: function (comment) {
          var comments = this.state.data;
          var newComments = comments.concat([comment]);
          this.setState({data: newComments});
          $.ajax({
            url: this.props.url,
            dataType: 'json',
            type: 'POST',
            data: comment,
            success: function (data) {
              this.setState({data:data});
            }.bind(this)
          });
        },
        getInitialState: function () {
          return {data: []};
        },
        componentWillMount: function () {
          this.loadCommentsFromServer();
          setInterval(this.loadCommentsFromServer, this.props.pollInterval)
        },
        render: function () {
          return (
            <div className='commentBox'>
              <h1>Comments</h1>
              <CommentList data={this.state.data}/>
              <CommentForm onCommentSubmit={this.handleCommentSubmit}/>
            </div>
          );
        }
      });
      /*
      var CommentBox = React.createClass({
        render: function () {
          return (
            React.DOM.div({
              className: 'commentBox',
              children: 'Hello, world! I am a CommentBox.!!!'
            })
          );
        }
      });
      */
      var CommentList = React.createClass({
        render: function () {
          var commentNodes = this.props.data.map(function (comment) {
            return <Comment author={comment.author}>{comment.text}</Comment>
          });
          return (
            <div className='commentList'>
              {commentNodes}
            </div>
          );
        }
      });

      var CommentForm = React.createClass({
        handleSubmit: function () {
          var author = this.refs.author.getDOMNode().value.trim();
          var text = this.refs.text.getDOMNode().value.trim();
          if (!text || !author) {
            return false;
          }
          this.props.onCommentSubmit({author: author, text: text});
          this.refs.author.getDOMNode().value= '';
          this.refs.text.getDOMNode().value = '';
          return false;
        },
        render: function () {
          return (
            <form className='commentForm' onSubmit={this.handleSubmit}>
              <input type='text' placeholder='Your name' ref='author' />
              <input type='text' placeholder='Say something' ref='text' />
              <input type='submit' value="Post" />
            </form>
          );
        }
      });

      var Comment = React.createClass({
        render: function () {
          var rawMarkup = converter.makeHtml(this.props.children.toString());
          return (
            <div className='comment'>
              <h3 className='commentAuthor'>
                {this.props.author}
              </h3>
              <span dangerouslySetInnerHTML={{__html: rawMarkup}} />
            </div>
          );
        }
      });


      React.renderComponent(
        <CommentBox url='comments.json' pollInterval={2000} />,
        document.getElementById('content')
      );
    </script>
  </body>
</html>

原文

為什麼使用 React ?

React 是 Facebook 和 Instagram 用來建置使用者介面的函式庫。近來有許多人考慮使用 React 來處理 MVC 中的 V 的部分。
Facebook 創造了 React 是為了解決構建一個大型且資料不斷變動的應用程式時遇到的問題。
為了達到這個需求,React 採用了兩個主要的核心概念。

單純性(Simple)

任何一個時間點您的應用程式都應該傳達同樣的資訊,且當在背後的資料改變的時候 React 會自動管理關於界面 UI 上的更新。

定義的方式(Declarative)

這個翻譯有點不是很精準,大略是說您不應該從外部去控制元件該如何更新資料,而是在元件內部定義資料是哪來的怎麼更新。
當資料發生變更的時候,概念上就是 React 點擊了 refresh 按鈕,接著元件會自己知道只變更有更新的部分。

構建可組合的元件

什麼是元件呢?在 React 中所有的東西都是元件。事實上,使用 React 就是在建立這些可以重複使用的元件。因此你的目標就是封裝,組件化,重複使用,關注點分離。

小弟認為最好實務上的舉例就是類似 WinForm 或 WPF 的控制項(dll)。用起來就像 XAML ,透過屬性傳遞參數,至於程式行為已經都封裝在類別裡面了。

給個 5 分鐘看看

React 挑戰了很多傳統的做法,而且第一次大略看到這東西也許會覺得它瘋了嗎。給個 5 分鐘閱讀官方文件:這些概念已經建立了上千個元件且我們應用在 Facebook 和 Instagram。

呈現資料

在開發網頁的過程中,大部份的工作就是讓你的 UI 界面呈現資料。React 讓界面可以方便地去呈現資料並且當資料有異動的時候自動更新。

入門

讓我們看看這個簡單的範例。建立 hello-react.html 檔案並加入如下的程式碼:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello React</title>
    <script src="http://fb.me/react-0.11.1.js"></script>
    <script src="http://fb.me/JSXTransformer-0.11.1.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/jsx">

      // ** 您的程式碼應該在此 **


    </script>
  </body>
</html>

針對檔案剩下的部分,我們只會專注在關於 Javascript 程式碼的部分,舉例來說你應該把下面的 Javascript 程式碼插入在像上面的 HTML 指示的位置上。

/** @jsx React.DOM */

var HelloWorld = React.createClass({
  render: function() {
    return (
      <p>
        Hello, <input type="text" placeholder="Your name here" />!
        It is {this.props.date.toTimeString()}
      </p>
    );
  }
});

setInterval(function() {
  React.renderComponent(
    <HelloWorld date={new Date()} />,
    document.getElementById('example')
  );
}, 500);

及時更新

在瀏覽器打開 hello-react.html ,注意到 React 只會自動更新時間的部分。有 Javascript 經驗的開發者會注意到,這邊我們是透過 setInterval() 每 500 毫秒 renderComponent() 一次。但是如果你在 input 中輸入一些資料會發現它會維持不變,並不會被清空。React 會正確地為你處理這些。
從這邊我們可以得知 React 除非必要,平常不會直接操作 DOM 元素。它採用一種快速的方式-在內部模擬 DOM 來進行比較和計算如何有效率的操作 DOM。
一般來說我們會使用 props 把資料輸入元件,它是 Properties-屬性的縮寫 。props 是透過 JSX 標簽裡的屬性(attributes)來傳遞的。
你應該想到這些 props 在元件內部是靜態不變的,我們不會在元件內部變更 this.props 的值。

一般來說我們會把資料放在 <Tag attribute={data here}> 屬性,從外部把資料傳進去,接著在內部透過 this.props 取得資料或函式。

元件本身就像函數

React 元件非常單純,你可以把他們想成單純的函數,在取得 propsstate (後續介紹) 後渲染產生 HTML,這讓整個元件的架構比較好理解。

注意:React 的限制,React 元件只有一個根節點,如果你想要傳回多個節點的結構就必須把它們全部包含在根節點裡面。

JSX 語法

我們非常相信元件比樣板搭配呈現邏輯更可以正確的達到關注點分離的目的。我們認為標簽和程式碼應該緊密的聯結在一起。
此外呈現邏輯通常非常複雜,使用樣板語言去表示會變得很笨重。
我們發現,針對這個問題最佳的解決方式就是直接從 Javascript 把行為和標簽一起產生。如此才能完全發揮實際程式語言俱有的表述能力去建立 UI。為了讓這一切簡化,我們加入了 JSX 的功能,透過類似 HTML 的語法去產生標記物件。
JSX 讓你可以在 Javascript function 裡面使用類似 HTML 的語法,舉例來說在 React 裡面要做一個超連結如果不用 JSX 的話你得這樣寫 React.DOM.a({href='http://facebook.github.io/react'}, 'Hello React!') ,如果是透過 JSX 則變成這樣 <a href="http://facebook.github.io/react/">Hello React!</a>。我們覺得這樣做會讓建置設計 React 應用程式或元件變得簡單。但因為採用了 JSX 在部署專案或開發流程上會有一些需要額外加入處理的事情,基於每個開發者都有自己的一套工作流程和偏好,所以 JSX 不是必要的,它是附加可選的。
JSX 非常類似 HTML,但事實上它們並不是完全一樣。查閱JSX gotchas 可以知道哪些關鍵不同的地方。

最後,最簡單開始學習 JSX 的方式就是使用 JSXTransformer 。但是我們強烈建議不要在 Production 上面使用,你可以使用react-tools預先編譯。

深入 JSX

JSX 是一種在 Javascript 中使用的 XML 語法,目的是用來轉換成原生的 Javascript。React 官方推薦使用。

是為了簡化類似 OOP 實例化多層架構物件。舉個例子像下面的虛擬碼

List l = new List();
Item i1 = new Item();
Item i2 = new Item();
l.Add(i1);
i.Add(i2);

這樣如果情況更複雜的時候會很難維護,換個方式如果是用 XML 的方式

<List>
  <Item name='i1' />
  <Item name='i2' />
</List>

感覺會比較好維護。 JSX 就是把下面的 XML 語法轉換成 Javascript 的一種工具。

注意: 不要忘記在程式碼開頭的 /** @jsx React.DOM */ 他不是一般的註解,這是告訴 JSX 編譯器要編譯這個檔案給 React 使用。
如果你沒有加入這段編譯指示,程式將不會被編譯。因此您可以安心的使用 JSX transformer,因為它並不會隨便編譯其他的 Javascript 檔案。

透過觀察下面這段簡單的程式碼,我們試著把 /** @jsx React.DOM */ 移除。會發現整段 <script type='text/jsx'> 都不會執行。其他功能都不會被影響。所以在已有的專案裡面使用 React ,就算他出錯了基本上也不會導致其他功能異常。

<!-- ex-1.html -->
<html>
  <head>
    <title>Hello React</title>
    <script src="http://fb.me/react-0.8.0.js"></script>
    <script src="http://fb.me/JSXTransformer-0.8.0.js"></script>
    <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
  </head>
  <body>
    <div id='content'></div>
    <h1>Static H1</h1>
    <script>
    $(function () {
      $('h1').click(function () {
        console.log('h1 click');
      });
    });
    </script>
    <script type='text/jsx'>
      /** @jsx React.DOM */
      console.log('jsx');
      var HelloWorld = React.createClass({
        render: function () {
          return (
            <div>Hello World</div>
          )
        }
      });
      React.renderComponent(<HelloWorld />, document.getElementById('content'));
    </script>
  </body>
</html>

為什麼使用 JSX ?

JSX 並不是強迫使用的,在不使用 JSX 的情況下,我們就得使用 React.DOM 提供的函式來建立註記標簽。上一篇我們提到 React 除非必要,平常不會直接操作 DOM 元素。它採用在內部模擬 DOM 來進行比較,計算如何有效率的操作 DOM 的方式在運作。使用 JSX 的最終目的是要幫助你用 XML 的編排方式轉成 React.DOM 的語法。
例如我們想建立一個超連結

var link = React.DOM.a({href: 'http://facebook.github.io/react'}, 'React');

如果情況在複雜一點可以比較一下下面的程式碼:

/* JSX*/
React.createClass({
  render: function () {
    return (
      <div>
        <h1>Title</h1>
        <a href='http://andyyou.logdown.com/'>AndyYou Blog</a>
      </div>
    )
  }
})
/* Origin */
React.createClass({
  render: function () {
    return (
      React.DOM.div(null,
        React.DOM.h1(null, "Title"),
        React.DOM.a( {href:"http://andyyou.logdown.com/"}, "AndyYou Blog")
      )
    )
  }
})

官方推薦使用 JSX 的理由如下:

  • 可以輕易的檢視整個 DOM 的結構,就跟你使用 HTML 一樣。
  • 方便維護修改。
  • 概念上非常相似于 MXMLXAML

關於轉換

JSX 的目的是從類似 XML 的語法轉換成原生的 Javascript。XML 標簽元素和屬性會被轉換成 function 和物件。看看下面的虛擬碼

var Nav;
// 編譯前 JSX

var app = <Nav color='blue' />
// 編譯後 JS

var app = Nav({color:'blue'})

注意: 為了能正確的使用 <Nav />Nav 變數必須在 scope 裡。
JSX 也可以在元素中嵌入子元素。

var Nav, Profile;
// Input (JSX):

var app = <Nav color="blue"><Profile>click</Profile></Nav>;
// Output (JS):

var app = Nav({color:"blue"}, Profile(null, "click"));

我們根據上面的說明實際撰寫一段可以運作的程式碼如下

/** @jsx React.DOM */
var Profile = React.createClass({
  render: function () {
    return (
      <a href='http://andyyou.logdown.com/'>AndyYou</a>
    );
  }
});
var Nav = React.createClass({
  render: function () {
    return (
      <nav>
        <li><Profile /></li>
        <li>Home</li>
        <li>About</li>
      </nav>
    );
  }
});
React.renderComponent(<Nav />, document.getElementById('nav'));

使用 JSX 編譯工具 就可以看到 JSX 是如何被轉換的。或者你也可以使用我們提供的線上工具
查閱第一篇教學有關於如何使用編譯工具。

注意: 關於上面的虛擬碼是協助你理解關於 JSX 是如何轉換的,你不能夠直接使用虛擬碼的部分。

上面範例中加入了一段實作程式碼以方便學習實際演練。

React 和 JSX

ReactJSX 是兩種獨立的技術,但 JSX 是 React 衍伸的一個重要的觀念。 JSX 正確來說被用在兩個地方:

  • 構建 React DOM 元件(React.DOM.*)。
  • 在使用 React.createClass() 設計元件結構。

React DOM 元件

如果要建立一個 <div> 的標記物件,實際上是使用 React.DOM.div

var div =  React.DOM.div;
var app = <div className='appClass'>Hello, React!</div>

React 組合元件

如果你想建立一個組合元件,你應該先透過 React.createClass({/*....*/}) 建立一個類別,然後才能實例化。

var MyComponent = React.createClass({/*...*/});
var app = <MyComponent someProperty={true} />;

JSX 會自動根據變數名稱或 displayName 推斷元件的名稱,而 displayName 一般會在 debug 時使用。
查閱複合式元件學習到更多關於多個元件組成的介紹。

注意:由於 JSX 是一種 Javascript,所以不能夠在標簽裡面直接使用 classfor 當作屬性名稱,替代的方式是 React 會用 classNamehtmlFor 取代。

易於使用的 DOM / 透過 JSX 使程式更加簡潔

如果每一個元素都要自行定義,事情肯定是非常單調乏味(舉例來說: div, span, h1, h2, ...)。JSX 提供一個便利的處理方式就是在 @jsx 註解的區塊指定一個變數,JSX 將會用這個指定的領域裡面搜尋 DOM 元件。

/**
 * @jsx React.DOM
 */
// 有了上面的 @jsx React.DOM 就可以使用一般的 DOM 元素(div、span、h1、ul、li、table 等)

// 如果想用其他非 HTML 標簽才需要定義,如下面的Nav

var Nav;
// Input (JSX):

var tree = <Nav><span /></Nav>;
// Output (JS):

var tree = Nav(null, React.DOM.span(null));

提醒:JSX 只會單純的把元素轉成函式來呼叫,例如: React.DOM.div(),而且並不是表示 DOM 。在註解裡面指定的參數只是為了解決常用的標簽元素。一般來說 JSX 是不俱有 DOM 概念的。

Javascript 表達式

屬性表達式

為了使用 Javascript 來取得屬性的值,JSX 使用 {...} 大括號而不是 "..." 雙引號。

// Input (JSX):

var person = <Person name={window.isLoggedIn ? window.name : ''} />;
// Output (JS):

var person = Person({name: window.isLoggedIn ? window.name : ''});

子元素表達式

同樣的,Javascript 也可以拿來放入子元素:

// Input (JSX):

var content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
// Output (JS):

var content = Container(null, window.isLoggedIn ? Nav(null) : Login(null));

註解

在 JSX 加入註解跟 Javascript 一樣。

var content = <Container>{/* this is a comment */}<Nav /></Container>;

注意:我們已經知道 JSX 會被轉成 Javascript,上面提到的註解,事實上並不能隨處亂加,舉個例子:

var Simple = React.createClass({
  render: function () {
    return (
      <a href='http://andyyou.logdown.com/'>AndyYou</a>
    );
  }
});
var Container = React.createClass({
  render: function () {
    return (
      <div>
        <li><Simple />{ /* 亂加的註解 */}</li>
      </div>
    );
  }
});

React.renderComponent(<Container />, document.getElementById('content'));

觀察被編譯過的檔案我們可以看到錯誤

var Simple = React.createClass({displayName: 'Simple',
  render: function () {
    return (
      React.DOM.a( {href:"http://andyyou.logdown.com/"}, "AndyYou")
    );
  }
});
var Container = React.createClass({displayName: 'Container',
  render: function () {
    return (
      React.DOM.div(null,
        React.DOM.li(null, Simple(null ), /* 亂加的註解 */)

      )
    );
  }
});
React.renderComponent(Container(null ), document.getElementById('content'));

重點

JSX 類似于其他嵌入 Javascript XML 式的語言或專案。JSX致力于下列的方向:

  • JSX 專注于語法解析轉換。
  • JSX 不相依於其他外部的函式庫。
  • JSX 不會影響既有的 Javascript 語法。

JSX 類似於 HTML 但是並不是完全跟其相同,詳見 JSX的陷阱 可以知道一些關鍵的不同。

JSX 常見的陷阱

JSX 看起來像 HTML 但有一些您應該知道關鍵性的差異。
注意:對於和 DOM 之間的差異,例如行內式屬性設定(inline style),請查閱這裡

DOM 的差異:
React 為了跨瀏覽器和提升效能的因素,實作一套和瀏覽器本身無關的 events 以及模擬 DOM 的機制。我們可以借由這個機制處理一些關於原始 DOM 設計上一些不足的地方。

  • 所有的 DOM 屬性 PropertiesAttributes (包含事件)都應該使用駝峰式命名 camelCased ,這和一般的 Javascrpt 程式碼風格一致。我們故意在這邊違背 html 規格 ,因此這和 html 規格是不同的。
  • style 屬性透過 Javascript 物件和駝峰式的屬性來設定,而不是 CSS 字串。所以設定 CSS 的語法風格會和 DOM, Javascrit 屬性一致,外加這麼做可以防止 XSS 攻擊。
  • 所有在事件符合 W3C 規範,且所有事件(包含 submit)傳遞都遵照 W3C 規範,查閱 Event System 取得更多資訊。
  • 關於 onChange 事件行為就跟你所期待的一樣,當一個表單欄位改變了,事件就會被觸發,而不是在 onblur 失去焦點的時候才觸發。 我們特意違背現有的瀏覽器行為,因為原始的 onChange 事件行為跟其名稱並不符合,React 需要正確的用到這個 Event ,當使用者輸入資料的同時 React 就會及時反應。查閱Forms得知更多資訊。
  • 表單輸入的屬性例如 value checked 更多關於一些命名,用法,等請查閱 Forms

移除空白字元

JSX 不像 HTML 在渲染時如果在同一個點重複空白字元會保留一個其他自動移除,關於這點 JSX 會移除所有在 { } 之間的空白。如果你需要加入空白字元則要使用 {' '}

<div>{this.props.name} {' '} {this.props.surname}</div>

如果你對這個設計有什麼想法,歡迎加入Issue #65討論。

HTML 字元實體

您可以插入 HTML 字元實體在 JSX 裡:

<div>First &middot; Second</div>

如果你想要顯示一個 HTML 字元實體在動態的內容中,你會遇到重複跳脫字元的問題。因為 React 為了防止 XSS 會把所有要呈現的文字都先跳脫(escapes)。

// 不好的示範: 會輸出 "First &middot; Second"
<div>{'First &middot; Second'}</div>

這裡有一些方式可以解決這個問題。最簡單的方式就是在 Javascript 直接寫 unicode,不過你需要確定檔案被存成 UTF-8 格式。

<div>{'First · Second'}</div>

一個更安全的替代方式式找到 unicode 對應的編碼

<div>{'First \u00b7 Second'}</div>
<div>{'First ' + String.fromCharCode(183) + ' Second'}</div>

也可以把字串混合進陣列裡面

<div>{['First ', <span>&middot;</span>, ' Second']}</div>

當你要插入 HTML 的時候你可以用這最後一招

<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />

自定 HTML 屬性

如果你傳給 HTML 元素的屬性並不在 HTML 規範中,React 並不會渲染它。如果你想要自訂一個屬性(attribute)。你應該使用前綴詞 data-

<div data-custom-attribute="foo" />

無障礙網站的話屬性使用 aria- 開頭。

<div aria-hidden={true} />

互動式動態 UI

你已經學會如果使用 React 呈現資料了。現在讓我們的界面增加互動的功能。

範例

/** @jsx React.DOM */

var LikeButton = React.createClass({
  getInitialState: function() {
    return {liked: false};
  },
  handleClick: function(event) {
    this.setState({liked: !this.state.liked});
  },
  render: function() {
    var text = this.state.liked ? 'like' : 'unlike';
    return (
      <p onClick={this.handleClick}>
        You {text} this. Click to toggle.
      </p>
    );
  }
});

React.renderComponent(
  <LikeButton />,
  document.getElementById('example')
);

事件處理與合成事件

跟你在 HTML 裡替標簽增加事件處理程序一樣,在 React 中你必須要駝峰式的命名來設定事件。例如:onClick
React 保證實做的合成事件在 IE8 以上瀏覽器裡定義的所有事件行為是一致的。也就是說不管你使用哪種瀏覽器 React 都知道如何按照 Spec 規範傳遞(Bubble)和截取事件,並傳遞給事件處理程序確保一切都和 W3C 規範的一樣。
如果你想使用 React 在觸控裝置上(例如: 智慧型手機和平板),你可以呼叫 React.initializeTouchEvents(true) 開啟他們。

表面之下的機制:自訂繫結和委派

React 內部做了一些處理以確保你的程式執行的效能不會太糟和容易理解。

  • 自動繫結:在 Javascript 中當一個 callback 函式被建立時你通常需要明確的將它跟某個物件的方法關聯在一起使得我們可以確定資料是正確的。在 React ,每一個方法都是自動跟元件綁定。React 會暫存已綁定的方法以增加 CPU 和記憶體的使用效率。
  • 事件委派:React 實際上並沒有附加任何事件到元素本身。當 React 啟動時,它會在最上層啟動一個事件監聽器監聽所有的事件。當一個元件載入或取消載入的時候,事件處理程序會自動從內部增加或移除。一旦事件被觸發,React 會知道如何調派以及使用對應的程序,如果裡面沒有對應的事件,React 則不會執行任何動作。如果你想學習關於增加處理速度的知識可以參考David Walsh's excellent blog post

元件就是狀態機

React 認為 UI 就是一個簡單的狀態機。從 UI 的角度思考,UI 本身俱有多種狀態,並且負責把這些狀態輸出呈現。這樣做可以輕易讓你的 UI 保持一致。
在 React 中,你只要單純更新元件的狀態,接著根據狀態渲染輸出新的 UI。React 透過較有效率的方式協助你更新 DOM 。

狀態如何運作

比較普遍的方式通知 React 資料已經變動了是透過呼叫 setState(data, callback) ,這個方法會把資料 data 整合進 this.state 接著重新渲染元件。當原件完成這個動作,你可以額外的加入 callback 做後續處理,當然也可以不加。大多的狀況你不會需要提供 callback 因為 React 會自動及時的更新資料。

什麼元件應該有狀態?

大部分的原件都應該會從 props 取得一些資料,然後輸出。然而有些時候你還是需要回應使用者的操作,伺服器的請求,或一些隨著時間變化的資料。這個時候我們就會使用狀態 state
試著讓大多數的元件盡可能沒有狀態。透過這種方式你可以獨立各種狀態的邏輯盡可能減少複雜的邏輯,這會使你的應用程式比較容易理解。
一個比較常見的模式是先建立一些沒有狀態的元件,它們只負責輸出資料,然後在它們的上層有一個負責管理狀態的元件再把狀態資訊透過 props 傳給子元素。這個有狀態的元件內部封裝所有邏輯和方法,透過屬性宣告的方式底下的元件只要負責渲染資料。

該如何使用狀態?

狀態應該負責管理資料,實務上就是元件內的事件處理程序在資料發生異動的時候被觸發然後更新 UI 。在實際的程式裡這些資料通常是很小的 JSON。當建立一個管理狀態的元件時,盡可能思考最簡化的表達方式,且資料只放在 this.state 中。在 render() 方法內部則根據這個狀態單純的計算出你需要的資訊就好。
你會發現思考並以這種方式寫程式往往是最正確的。因此在狀態內部增加任何多餘的資料或計算意味著你需要去處理多餘的東西以確保資料是一致的,不要一味的依賴 React 計算處理資料。

狀態不應該這樣用?

this.state 應該只存放 UI 需要呈現的資料,不應該包含:

  • 已處理完成的資料:不用預先計算處理狀態的資料。在 render() 裡面計算資料是比較容易確保 UI 的資料是正確的。舉例來說如果有一個項目清單的陣列,你想要輸出清單數量,應該單純的在 render() 裡面使用 this.state.listItems.length + ' list items' 而不是把值直接存在 this.state
  • React 元件:在 render() 裡面根據 props state 去建置。
  • 重複存取在 props 裡的資料:props 應該是正確資料的來源,因為 props 可能隨著時間改變。適當的把 props 存在 state 可以協助我們取得之前的資料。

補充說明:
在上面的文章中我們了解到通常會使用一個主要的元件負責管理狀態,而其他子元件需要的時候則直接使用主元件的 props 和方法,下面的補充範例我們將簡單的示範一些關於使用事件的方式:

event.js
/** @jsx React.DOM */
/* 建立一個 Clicker 元件類別 */
var Clicker = React.createClass({
  /* 透過 render() 輸出三個超連結並使用主元件的事件。 */
  render: function () {
    var bind = this.handleBind.bind(this);
    return (
      <div>
        { /* 下面這個超連結示範了一般使用主元件的方法  */}
        <a onClick={this.handleNormal}>Normal</a>
        { /* 使用 bind() 的目的是為了在 function 內再使用主元件的函式 */}
        <a onClick={bind}>Bind</a>
        { /* 在 0.4.0 版之後為了簡化程式碼,預設就是 autoBind。 */}
        <a onClick={this.autoBindClick}>AutoBind</a>
      </div>
    );
  },
  handleNormal: function (event) {
    alert('Normal Event');
  },
  handleBind: function (event) {
    alert(this.ALERT_BIND); /* 取用主元件的函式 */
  },
  autoBindClick: function (event) {
    alert(this.ALERT_AUTOBIND);
    },
  ALERT_BIND: 'Bind Event',
  ALERT_AUTOBIND: 'Auto Bind Event'
  // oldAutoBindClick: React.autoBind(function () {...})

});
React.renderComponent(<Clicker />, document.getElementById('example'));

其他資訊

React.autoBind() 移除
Javascript bind()

複合式元件

到目前為止我們看過了如何建立一個單一的元件去呈現資料以及回應使用者的操作。接下來讓我們來看看 React 的另一個重要的功能:可組成。

動機:關注點分離

模組化建立可重複使用的界面元件,透過使用 function 或類別,讓我們可以在開發時得到一些益處。具體來說可以針對應用程式的功能分離不同的關注點,不過還是請你在建立新元件的時候儘量保持單純。針對應用程式自行設計元件庫,你的 UI 也比較容易和你的應用整合在一起。

在使用 jQuery 和其他第三方套件時候,我們常常會因為作者設定的 HTML 結構(樣板)打亂了我們既有的編排習慣,在 React 中我們通常只要在需要的地方放入一個 <div> ,接著實際產出的 HTML 和功能會透過 Javascript 直接注入。我們只要知道怎麼用元件就好,而不需要去組織樣板。

範例

讓我們建立一個單純的 Avatar(頭像)元件,它使用 Facebook Graph API 來取得個人資料和大頭照然後顯示。

/** @jsx React.DOM */

var Avatar = React.createClass({
  render: function() {
    return (
      <div>
        <ProfilePic username={this.props.username} />
        <ProfileLink username={this.props.username} />
      </div>
    );
  }
});

var ProfilePic = React.createClass({
  render: function() {
    return (
      <img src={'http://graph.facebook.com/' + this.props.username + '/picture'} />
    );
  }
});

var ProfileLink = React.createClass({
  render: function() {
    return (
      <a href={'http://www.facebook.com/' + this.props.username}>
        {this.props.username}
      </a>
    );
  }
});

React.renderComponent(
  <Avatar username="pwh" />,
  document.getElementById('example')
);

在上面的範例中,Avatar 元件實例裡面俱有 ProfilePicProfileLink 兩個元件。在 React 裡面主元件就是最上層的元件應該要提供 props 所需的資料。更精准一點來說,如果有一個元件X被寫在元件Y的 render() 裡面,這表示 Y 是擁有者-主元件也就是那個負責掌管狀態的元件。如同之前討論的一個元件不能更動自己的 props,而是透過上層元件去設定。通過屬性當接點我們可以保證 UI 的資料永遠是來自同一個地方以確保資料一致性。
有個重要的觀念是釐清這些被擁有者的關係和主從關係。被擁有者關係是 React 所俱有的,而元素地主從關係(父元素及子元素關係)就是單純的 DOM 結構。拿上面的範例說明:Avatar 擁有 divProfilePicProfileLink 物件。div 只是父元素,但他不是ProfilePicProfileLink 的擁有者(主元件)。

從程式的概念上來理解,一個 Avatar 是透過 React.createClass() 先建立類別,然後 new (React.renderComponent(<Avatar />, [domTag]))產生的實例物件來使用,每一個物件本身都有自己的 this.props 屬性。且資料通常是透過 <Avatar username={data} /> 傳入的。元件的資料狀態通常避免從外部影響,帶入參數之後就讓元件自己內部去處理。看看編譯過的程式碼會比較好理解 React.renderComponent(Avatar({username:'pwh'}), document.getElementById('example'));,所以擁有者元件是 Avatar 而不是 render() 裡面的 <div>。當然所謂的控管資料和狀態就是 Avatar 。而 <div> 只不過是用來輸出 DOM 結構的父元素。

子元件

當你建立了一個 React 物件,你可以包含其他的 React 元件或 Javascript 表示式。

<Parent><Child /></Parent>

讓我們根據上面的說明再提出一個範例

/** @jsx React.DOM */
var Avatar = React.createClass({
  render: function () {
    return (
      <div>
        <ProfilePic username={this.props.username} />
        <ProfileLink username={this.props.username} />
        {this.props.children /* 載入子元素 */}
      </div>
      /* 複習一下一個元件只能有一個根節點,你不能在這邊再加入一個 <div> */
    );
  }
});
var ProfilePic = React.createClass({
  render: function () {
    return (
      <img src={'http://graph.facebook.com/' + this.props.username + '/picture'} />
    );
  }
});
var ProfileLink = React.createClass({
  render: function () {
    return (
      <a href={'http://www.facebook.com/' + this.props.username}>
        {this.props.username}
      </a>
    );
  }
});
React.renderComponent(<Avatar username='andyyu0920'><ProfileLink username='phw' /></Avatar>, document.getElementById('example'));

父元件可以透過 this.props.children 讀取子元件。

子元件調和(Reconciliation)

調和的意思是 React 更新渲染 DOM 的處理過程。一般來說子元件會根據他們的順序重新被調整輸出。舉下面的例子來說

// Render Pass 1
<Card>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</Card>
// Render Pass 2
<Card>
  <p>Paragraph 2</p>
</Card>

我們會直覺的認為 <p>Paragraph 1</p> 被移除,但實際上 React 會重新調和 DOM ,他會把第一個元素的內容換掉,接著刪除最後一個元素。

/** @jsx React.DOM */
var Card = React.createClass({
  render: function () {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
});
React.renderComponent(
  <Card>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
  </Card>,
  document.getElementById('example')
);
React.renderComponent(
  <Card>
    <p>Paragraph 2</p>
  </Card>,
  document.getElementById('example')
);

為了更理解上面說的我們實作了另一個範例

/** @jsx React.DOM */
var Card = React.createClass({
  getInitialState: function () {
    return {children: [<input type='text' />, <p>Paragraph 1</p>, <p>{(new Date().toTimeString())}</p>]};
  },
  render: function () {
    return (
      <div>
        {this.state.children}
      </div>
    );
  }
});
var card = React.renderComponent(
      <Card />,
      document.getElementById('example')
    );
setInterval(function() {
  card.setState({children: [<input type='text' />, <p style={{display: 'none'}}>Paragraph 1</p>, <p>{(new Date().toTimeString())}</p>,<p>Paragraph 2</p>]});
}, 500);

直覺上我們會覺得 <input> 會被重新輸出,但是當我們在輸入框裡面留下資料的時候會發現他並沒有變成空白。總結來說 React 並不是單純直接把元件輸出,而是在內部經過比對處理後只更新異動的部分。

內嵌子元件狀態

對大多數元件來說,上面說的這種機制通常沒有什麼問題,然而對控管 this.state 的元件來說這可能會有問題。
一般情況下你可以透過隱藏元素來取代刪除他們。也就是說通常元件的結構定義完成之後我們通常不會去破壞任何一個節點。

// Render Pass 1
<Card>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</Card>
// Render Pass 2
<Card>
  <p style={{display: 'none'}}>Paragraph 1</p>
  <p>Paragraph 2</p>
</Card>

動態的內嵌子元件

當情況變得更複雜,內嵌的子元件被重新排列,例如顯示搜尋結果,或者要加入一些新的元件。在這些情況下每個元件唯一的識別子或狀態必須維持在 render() 傳遞。

render: function() {
    var results = this.props.results;
    return (
      <ol>
        {this.results.map(function(result) {
          return <li key={result.id}>{result.text}</li>;
        })}
      </ol>
    );
  }

當 React 調和(重新調整)這些帶有 key 的子元件時,這可以確保任何有 key 的元件都將被重新載入(而不是被破壞)或破壞(而不是重複使用)。
我們來寫段範例驗證

/** @jsx React.DOM */
var data = [{id: 1, text:'A'}];
var List = React.createClass({
  render: function () {
    var results = this.props.results;
    return (
      <ol>
        {results.map(function (result) {
          return <input type='text' key={result.id}/>;
        })}
      </ol>
    );
  }
});
React.renderComponent(<List results={data} />, document.getElementById('example'));

setInterval(function() {
  data[0].id += 1;
  React.renderComponent(<List results={data} />, document.getElementById('example'));
}, 5000);

<input> 輸入值之後 5 秒後因為 key 變換了所以你的輸入的值就被清空了。

資料流

在 React ,資料是從主元件透過 props 傳遞就如同之前說過的。實際上這是一個單向的資料繫結。主元件負責把資料繫結到子元件的 props ,主元件可以基於 propsstate 進行計算,由於這個流程會發生遞迴所以資料會自動映射至使用的地方。

關於效能

你也許會思考這樣的模式當 React 需要修改大量資料和節點的時候效能會不佳,好消息是 Javascript 本身是非常快速的,而且 render() 往往不會太複雜,因此大部份的應用程式速度都非常快。此外問題幾乎都在 DOM 的更動並不是 Javascript 而且 React 將會使用 batchingchange detection 優化這些。
然而有些時候你真的想要調整這些效能的問題,此時你可以覆寫 shouldComponentUpdate() 方法,透過回傳 false 。React 將會略過這段處理。詳細參閱 the React reference docs

注意:當 shouldComponentUpdate() 傳回 false ,此時資料被改變了,React 就不能維持 UI 同步了。只有在有明顯效能問題的時候才使用它,且請確保你知道該如何使用。

重複利用元件

這裡的設計界面,指的是打破現有的設計元素(button, form, fields 等)組合出定義良好可重複使用的元件。這樣一來下次你需要建置一樣的界面的時候就可以少寫一些程式碼,同時也節省許多開發時間。

Prop 驗證

當你的應用程式不斷增加,透過設定 propTypes 有助你確保你的元件被正確的使用。。React.PropTypes 會產生一系列的驗證使得你可以確保收到的資料是正確的。
prop 提供一個無效的資料時就會發出一個錯誤的例外。讓我們看看下面的使用範例:

React.createClass({
  propTypes: {
    // 您可以替 prop 指定 Javascript 預設的型別

    // 這些都是可以選擇的

    optionalArray: React.PropTypes.array,
    optionalBool: React.PropTypes.bool,
    optionalFunc: React.PropTypes.func,
    optionalNumber: React.PropTypes.number,
    optionalObject: React.PropTypes.object,
    optionalString: React.PropTypes.string,

    // 可以明確的限制 prop 為列舉型別。

    optionalEnum: React.PropTypes.oneOf(['News','Photos']),

    // 限制某種 Class

    someClass: React.PropTypes.instanceOf(SomeClass),

    // 加上特上面說的任何一種型別加上必須的限制。

    requiredFunc: React.PropTypes.func.isRequired

    // 加上自訂的驗證

    customProp: function(props, propName, componentName) {
      if (!/matchme/.test(props[propName])) {
        throw new Error('Validation failed!')
      }
    }
  },
  /* ... */
});

下面提供一個範例,你可以試著把 isShow 改成非 boolean 的任何值。

/** @jsx React.DOM */
var Test = React.createClass({
  propTypes: {
    isShow: React.PropTypes.bool.isRequired
  },
  render: function () {
    return (
      <p>{this.props.isShow ? "true" : "false"}</p>
    );
  }
});
React.renderComponent(<Test isShow={true} />, document.getElementById('example'))

出現錯誤:

我們可以再多嘗試一些範例來更明確理解:

/** @jsx React.DOM */
 var Car = function (wheel, brand) {
   this.wheel = wheel;
   this.brand = brand;
 };
 Car.prototype.run = function () {
   console.log('go!');

 var Bike = function (wheel) {
   this.wheel = wheel;
 }
 var car = new Car(4, 'Toyota');
 var bike = new Bike(2);
 console.log(car
 var Test = React.createClass({
   propTypes: {
     vihicle: React.PropTypes.instanceOf(Car)
   },
   render: function () {
     return (
       <p>I drive {this.props.vihicle.brand} car and its has {this.props.vihicle.wheel} </p>
     );
   }
 });
 React.renderComponent(<Test vihicle={car} />, document.getElementById('car'));
 // React.renderComponent(<Test vihicle={bike} />, document.getElementById('bike'));

預設 Prop

React 可以讓你定義 props 的預設值

var ComponentWithDefaultProps = React.createClass({
  getDefaultProps: function() {
    return {
      value: 'default value'
    };
  }
  /* ... */
});

執行範例:

/** @jsx React.DOM */
var ComponentWithDefault = React.createClass({
  getDefaultProps: function () {
    return {value: 'B'}
  },
  render: function () {
    return (
      <div>{this.props.value}</div>
    );
  }
});
React.renderComponent(<ComponentWithDefault value='A' />, document.getElementById('example'));

getDefaultProps 將會把預設值暫存起來,以確保 this.props.value 有值。以上面的實際範例來說如果你不帶值則值會是 B ,如果有值則預設值會被取代。這使得你可以放心使用 props ,而無需反覆撰寫脆弱的代碼來處理元件。

傳遞 Props 的捷徑

常見的 React 元件是用基本 HTML 組合的延伸。通常你會想要傳遞屬性給 HTML 元素,React 提供了 transferPropsTo() 方法可以把屬性帶入讓你少打一些字。

/** @jsx React.DOM */

var CheckLink = React.createClass({
  render: function() {
    // transferPropsTo() will take any props passed to CheckLink

    // and copy them to <a>

    return this.transferPropsTo(<a>{'√ '}{this.props.children}</a>);
  }
});

React.renderComponent(
  <CheckLink href="javascript:alert('Hello, world!');">
    Click here!
  </CheckLink>,
  document.getElementById('example')
);

上面這段說明,其實我們應該先釐清一般來說我們會把 React.renderComponent(<Component attribute='value'/>),當作呼叫函式或實例化物件,所以這邊的工作是把參數帶進去。接著因為 React 在 render() 的時候只能有一個根元素去包含其他元素。所以當你用了 transferPropsTo() 實例化產生物件那邊的屬性(attributes)會被整合到跟元素,接著你可以用 this.props.children 拿到在帶進來的子元素。
如果上面這段說明你還不是很明白,我們在比較一下沒有 transferPropsTo() 的寫法:

/** @jsx React.DOM */

var CheckLink = React.createClass({
  render: function() {
    // transferPropsTo() will take any props passed to CheckLink

    // and copy them to <a>

    return (<a href={this.props.href}>{'√ '}{this.props.children}</a>);
  }
});

React.renderComponent(
  <CheckLink href="javascript:alert('Hello, world!');">
    Click here!
  </CheckLink>,
  document.getElementById('example')
);

好吧!上面這個例子你並沒有少打幾個字,但如果當屬性很多的時候就真的是了XD

Mixin

在 React 中元件是是幫助你重複使用程式碼最好的方式,但是有些時候不同的元件可能會同樣的功能。有時候被稱作橫切關注點。React 提個 mixin 解決這個問題。
一個常見的情況是元件透過 setInterval() 更新,不過問題是當你不需要更新要取消 setInterval() 的時候。當你知道關於元件的生命週期,你就可以透過 mixin 讓有需要這個功能的元件共用。

/** @jsx React.DOM */

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.map(clearInterval);
  }
};

var TickTock = React.createClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the mixin
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

React.renderComponent(
  <TickTock />,
  document.getElementById('example')
);

注意當元件混入很多 mixin event 的時候,如果是生命週期的函式則保證被呼叫,但如果不是則會導致元件被破壞。

表單

表單元件像是 <input><textarea><option>和其他元件有些不同,因為他們可以被使用者操作而產生變化。這些元件提供一個介面好讓我們以表單的形式和使用者產生互動。

Props 的輸入與輸出

表單元件提供一些 props 會根據使用者的操作影響屬性值和呈現。

  • value<input> <textarea> 支援。
  • checkedcheckboxradio 支援。
  • selected<option> 支援。

在 HTML 裡面 <textarea> 的 value 是嵌在子元素就是像這樣 <textarea>value here</textarea>,但是在 React 你可以用 value 取代。
表單元件透過 onChange 設定 callback 可以達到監聽的功能,就是一旦這些值改變了就會觸發 onChangeonChange 會在使用者有以下狀況時被觸發:

  • <input><textarea> 的 value 改變時。
  • checked 改變時。
  • selected 改變時。

就像所有的 DOM 事件,onChange 支持所有原生元件且可以監聽到因氣泡傳遞產生的觸發。

元件約束

一個 <input> 一旦設定了 value 就是一個約束的元件。輸出後元素 <input> 的值將會永遠映射到 this.props.value 舉例來說

/** @jsx React.DOM */
var Test = React.createClass({
  render: function () {
    return (
      <input type='text' value='Hello!' />
    );
  }
});
React.renderComponent(<Test />, document.getElementById('example'));

value 一旦設定,你會發現不能改變了,像上面範例 value 永遠等於 Hello! 。任何輸入都無法改變值,因為 React 已經定義 valueHello!,如果你想要讓 <input> 可以被使用者操作你應該使用 onChange 事件:

/** @jsx React.DOM */
var Test = React.createClass({
  getInitialState: function () {
    return {value: 'Hello!'}
  },
  handleChange: function (event) {
    this.setState({value: event.target.value});
  },
  render: function () {
    // var value = this.state.value;

    return (
      <input type='text' value={this.state.value} onChange={this.handleChange} />
    );
  }
});

讓我們整理一下你在目前學習過程可能會產生的疑惑,就是 propsstate 的差異,首先是 props 不應該由元件本身去操作異動,應該只能夠在使用元件的時候當作帶入的參數(雖然的確是可以使用 setProps() 去設定,不過這違反官方的設計模式)。接著的工作就是負責傳遞資料。
this.state 可以當作參數傳遞資料也可以在元件內部去設置改變。換句話說,你應該只能在元件內部呼叫 setState() ,常見的應用都是用 this.state 處理關於使用者操作互動產生的結果。此外要注意的是如果你真的在子元件也使用了 state 那它跟主元件本身的狀態是分開獨立的。
另一個問題是為什麼 props 可以用 propTypes 驗證,而 state 沒有,主要是因為關於 state 完全是由元件設計者控制的,想像一下一般的情況下你設計了一個元件,而當其他人要使用的時候他應該只需要使用 React.renderComponent(<YourComponent />, document.getElementById('example')) 輸出元件即可,根據你提供的文件,它可以在屬性設定自己的參數 <YourComponent name="Andy" /> ,而 this.state 根本沒機會外露(修改元件除外)。所以如果你真的需要驗證 state 的時候就在適合的生命週期事件中處理就好了,例如 componentWillUpdate 事件。
根據上面這些說明,我們得到結論:就是一旦 <input>value 被綁定就不會變動了,我們稱為元件約束,所以應該在 value 綁入一個變數,而這個變數按照模式的規劃應該使用 this.state 取得值是比較正確的。

handleChange: function(event) {
    this.setState({value: event.target.value.substr(0, 140)});
  }

修改 handleChange() 範例就可以實作限制在 140 個字元。

不被約束的元件

如果一個 <input> 沒有設定 value 那它就是一個不被約束的元件。此時 input 的 value 就會是使用者輸入的值。

  render: function() {
    return <input type="text" />;
  }

上面這段程式碼並沒有設定 value ,使用者輸入的任何值都會立刻反應。想要在使用者改變欄位值的同時做些處理可以使用 onChange
如果你想在初始化的時候不要帶入空值,且可以隨著使用者操作回饋,你可以使用 defaultValue

render: function() {
  return <input type="text" defaultValue="Hello!" />;
}

其他類似的屬性像是<input> 還有 defaultChecked<select> 也有 defaultValue

進階議題

為什麼要約束元件?

React 在設計像是 <input> 這類表單元件時面臨一個表現時的挑戰。舉例來說在 HTML

  <input type="text" name="title" value="Untitled" />

在傳統 HTML 是表示初始化值設定為 Untitled 當使用者更新欄位時,元素取得的屬性將會改變,然而如果你使用 node.getAttribute('value') 其實他還是會返回 Untitled。讓我們直接看下面這段原始碼

<html>
  <head>
    <title>Hello React</title>
    <script>
    var val1, val2;
    function Log () {
      console.log(this);
      console.log(this.getAttribute('value'));
      console.log(document.getElementsByName(this.name)[0].value);
      console.log(this.value);
    }
    </script>
  </head>
  <body>
    <input type='text' name='i1' value='Untitled' onchange="Log.apply(this)" />
  </body>
</html>

試著輸入值觀察 console 。注意:HTML 的 onchange 行為是在你改變了值,滑鼠移出欄位之時才觸發。

React 是透過 <input value=' '/>value 影響元素,但是當我們改變欄位的時候卻只能從 node.value 而不是 node.getAttribute('value') 取得使用者改變的資料,所以如果要把這個值表現在元素上就只能把這個 node.value 放到 state 變數,然後屬性就使用 this.state.value。這也就是為什麼需要約束元件。
也因此當我們如下程式碼的時候

render: function() {
  return <input type="text" name="title" value="Untitled" />;
}

因為我們接不到 event.target.value 所以 React 會一直保持 Untitled

關於 Textarea

在 HTML一般設定 <textarea> 是用子元素去設定

<textarea name="description">This is the description.</textarea>

開發者可以很輕易的使用多行的內容,但因為 React 是 Javascript 所以當你想要換行的時候可以使用 \n

先看下面這段程式碼

/** @jsx React.DOM */
var Textarea = React.createClass({
  render: function () {
    return (
      <textarea value='value' defaultValue='default'>This is dog</textarea>
    );
  }
});
React.renderComponent(<Textarea />, document.getElementById('example'));

其實我們這三個設定都可以用,但是子元素會等於 defaultValue 所以當子元素和 defaultValue 都用的時候會產生錯誤 If you supplydefaultValueon a <textarea>, do not pass children.value 會覆寫 defaultValue 。為了避免混淆我們一般建議只用 value,接下來的用法就和上面提到的一樣。

關於 Select

一般我們使用 <select> 要指定選項是透過在 <option selcted>,在 React 為了讓元件方便操作我們改用下面的方式

/** @jsx React.DOM */
var Dropdown = React.createClass({
  render: function () {
    return (
      <select value={this.props.selected}>
        <option value="A">Apple</option>
        <option value="B">Banana</option>
        <option value="C">Cranberry</option>
      </select>
    );
  }
});
React.renderComponent(<Dropdown selected='C' />, document.getElementById('example'));

如果要是預設值也是用 defaultValue 如下

/** @jsx React.DOM */
var Dropdown = React.createClass({
  render: function () {
    return (
      <select value={this.props.selected} defaultValue="B">
        <option value="A">Apple</option>
        <option value="B">Banana</option>
        <option value="C">Cranberry</option>
      </select>
    );
  }
});
React.renderComponent(<Dropdown />, document.getElementById('example'));

與瀏覽器之間的運作

React 針對瀏覽器提供了十分強大的抽象化概念,讓你在大部份的情況下不必再直接操作 DOM ,不過有些時候或許還是需要單純的存取底層的 API(DOM API),可能是使用第三方函式庫或者事已經寫好的程式碼。

關於虛擬 DOM

React 快速的原因是因為它從來不直接影響 DOM。React 會負責在記憶體中持續維護一份 DOM 的表現結構。render() 方法負責回傳關於 DOM 的描速,React 就能得知其和記憶體中結構的差異,接著他會計算出最快的更新方式然後交給瀏覽器去影響 DOM。
此外,React 完整實作了對應的事件系統,所有物件的事件保證符合 W3C 的規範,且關於事件氣泡傳遞(bubbles)的行為在任何瀏覽器也都一致。甚至可以在 IE8 使用 HTML5 的事件。大多數的時候你的程式操作應該都會 React 所建構的"仿瀏覽器"的世界裡,因為它俱有高效能和相對容易使用。不過有些情況下,你可能會需要存取使用底層基本的 API,例如使用 jQuery 第三方套件,React 提供了一個後門允許你可以直接操作底層的 API。

# Refs 和 getDOMNode()
為了與瀏覽器互動,你需要使用指向 DOM 節點的參考物件。每一個 Mounted 的 React 元件都會有 getDOMNode() 的功能 ,你可以透過呼叫它取得該 DOM 的參考物件。

注意:getDOMNode() 只能在元件已經掛載完畢時使用(換句話說這表示該物件已經被渲染放置到 DOM 裡了)。如果你嘗試在元件尚未掛載完畢前呼叫這個 API 將會發生例外。

為了取得 React 元件的參考,你可以使用 this 取得目前元件或者使用 refs,使用 refs 則需要設定一個名稱,如下範例:

/** @jsx React.DOM */

var MyComponent = React.createClass({
  handleClick: function() {
    // 透過原生 API 明確的指示 input 為 focus 狀態。
    this.refs.myTextInput.getDOMNode().focus();
  },
  render: function() {
    // ref 屬性替元件增加一個參考然後你就可以在元件掛載完畢後使用 `this.refs`
    return (
      <div>
        <input type="text" ref="myTextInput" />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.handleClick}
        />
      </div>
    );
  }
});

React.renderComponent(
  <MyComponent />,
  document.getElementById('example')
);

更多關於 Refs

想要了解更多關於 refs 及如何有效的使用可以參考 refs 文件

元件的生命週期

元件的生命週期有三個主要的部分:

  • Mounting:元件正準備要被寫入 DOM
  • Updating:元件偵測到狀態的改變準備重新渲染。
  • Unmounting:元件正要被從 DOM 中移除。

React 根據生命週期提供了對應的方法(事件)讓你可以在對應的階段做一些處理。Will 的方法用在某些狀況準備發生之前,Did 的方法則表示該狀況已經發生後。

Mounting 掛載流程

  • getInitialState():當物件被調用時此方法會在寫入 DOM 之前被觸發,通常用來管理狀態的元件可以用這個方法初始化一些資料。
  • componentWillMount:當元件內部的結構處理完畢準備寫入 DOM 之前觸發。
  • componentDidMount(DOMElement rootNode):當元件被寫入 DOM 之後觸發。當初始化需要操作 DOM 元素就可以用這個方法。

Updating 更新流程

  • componentWillReceiveProps(nextProps):已掛載的元件收到新的 props 時被觸發。在這個方法裡你通常會去比較 this.propsnextProps 然後再用 this.setState 去改變狀態。
  • shouldComponentUpdate(nextProps, nextState):這個函式需要回傳一個布林值,當元件判斷是否需要更新 DOM 時會被觸發。你可以在這個方法裡面去比較 this.propsthis.statenextPropsnextState 來決定是否需要更新,回傳 false 則會跳過此次觸發不更新,如果你什麼都不回傳預設會當做 false
  • componentWillUpdate:例如在上面 shouldComponentUpdate 你回傳了 true ,元件確定要更新了,在準備更新前這個方法會被觸發。
  • componentDidupdate(prevProps, prevState, rootNode):更新後觸發。

Unmounting 卸載流程

  • componentWillUnmount():當元件準備要被移除或破壞時觸發。

掛載後才能使用的方法

  • getDOMNode():使用此方法會傳回一個 DOM 元素物件,透過這個方法你可以取得一個參考物件直接操作 DOM 節點。
  • forceUpdate():任何已掛載的元件,當你知道元件內部有些狀態已經改變但他不是透過 this.setState() 去修改值的時候可以呼叫這個方法強迫更新。

注意:componentDidMount()componentDidUpdate()rootNode 參數只是提供你一個比較方便的方式存取 DOM ,這和使用 this.getDOMNode() 是一樣的。
補上一段實作各種方法的範例您可以試著把註解的地方取消看看變化

/** @jsx React.DOM */
var Test = React.createClass({
  getInitialState: function () {
    console.log("> getInitialState()");
    return {user: 'AndyYou'};
  },
  componentWillMount: function () {
    console.log("> componentWillMount()");
  },
  componentDidMount: function (node) {
    console.log("> componentDidMount(node)");
    console.log(node.className);
    console.log(node.value);
    console.log(node.id);
    console.log(this.getDOMNode().className);
    console.log(this.getDOMNode().value);
    console.log(this.getDOMNode().id);
  },
  componentWillReceiveProps: function (nextProps) {
    console.log("> componentWillReceiveProps(nextProps)");
    console.log(nextProps);
  },
  handleChange: function (e) {
    console.log(e.target.value);
    this.setState({user: e.target.value});
  },
  shouldComponentUpdate: function (nextProps, nextState) {
    console.log("> shouldComponentUpdate(nextProps, nextState)");
    console.log("nextProps: ");
    console.log(nextProps);
    console.log("nextState: ");
    console.log(nextState);
    return true; /* need return true/false */
  },
  componentWillUpdate: function (nextProps, nextState) {
    console.log("> componentWillUpdate(nextProps, nextState)");
  },
  componentWillUnmount: function () {
    console.log("> componentWillUnmount()");
  },
  render: function () {
    return (
      <input type='text' id='foobar' value={this.state.user} className='nav' onChange={this.handleChange} />
    );
  }
});
var test = React.renderComponent(<Test title='Untitled' />, document.getElementById('example'));
// test.setProps({title: 'No'});

// React.unmountComponentAtNode(document.getElementById('example'));

// test.setState({user:'Calvert'});

支援的瀏覽器和兼容

在 Facebook 我們支援了包含 IE8 在內的舊瀏覽器,我們已經落實瀏覽器兼容很長一段時間了,這讓我們可以實作出有實用且遠見的 Javascript。這表示我們並沒有太多 Hack 特定瀏覽器產生鬆散的分支代碼,根據這些經驗我們可以確信我們的程式碼在任何瀏覽器都是可以正常運作的。舉個例子常見 +new Date() 這種寫法我們會改用 Date.now()
React Open Source 專案和 Facebook 內部使用的的是一樣的,我們已經證實並正在使用。
此外我們並不試圖讓實作兼容功能變成函式庫的一部份,如果每個函式庫都重新實作這些功能,為了支援老舊瀏覽器,你會反覆載入相同功能的程式碼。如果你需要支援舊的瀏覽器,可能你已經在使用 es5-shim

兼容

es5-shim.js 可以從 kriskowal's es5-shim 取得。下面是 React 支援老舊瀏覽器需要的東西

  • Array.isArray
  • Array.prototype.forEach
  • Array.prototype.indexOf
  • Array.prototype.some
  • Date.now
  • Function.prototype.bind

es5-sham.js 也可以從 kriskowal's es5-shim 取得,React 需要:

  • Object.create

React 的非最小化建置則需要 paulmillr's console-polyfill

  • console.l*

跨瀏覽器的議題

雖然 React 對於瀏覽器的抽象化過程處理的非常不錯,但是有些瀏覽器的限制或怪異的行為我們還是找不到解法

IE8 的 onScroll 事件

IE8 的 onScroll 事件不會造成事件的氣泡傳遞而且 IE8 也沒有定義對應的處理事件。目前關於在 IE8 的這個事件已經被忽略了。

Polyfills

Polyfilling 是由 RemySharp 所提出的術語,它是用來描速關於複製缺少的 API 和 API 功能的行為。你可以使用它撰寫應用程式的程式碼而不用擔心其他瀏覽器是不是支援。事實上,polyfills 並不是新技術也不是和 HTML5 捆绑到一起的。

Polyfills 是什麼?

讓我們直接來看實務 Polyfills 指的是什麼。例如使用 json2.js 就是一種 Polyfills。

if (typeof JSON.parse !=='function') {
  // Crockford’s JavaScript implementation of JSON.parse

}

上面這段程式碼表示; 如果瀏覽器本身可以執行 JSON.parse,那麼 json2.js 就不會重新定義或者干擾 JSON 物件。如果沒有原生的 API 可用,json2.js 就會執行一段 JavaScript 來實現這個功能,它和原生的 JSON API 是完全兼容的。最終的結果就是你可以在網頁上使用 json2.js 而不用考慮瀏覽器執行的是哪種程式碼。

關於 Refs

當你透過 render() 回傳你的 UI 結構之後,你可能想要從外部調用這個元件實例的方法。通常情況下為了取得一些元件或計算後資料你可能這樣做,但其實是不必要的,因為 React 通常會確保資料是最新的 props 且透過 render() 傳遞到子元件。不過的確有些情況還是會需要從外部調用方法。
想像下面這種狀況,當你想讓一個已存在的某元件的子元件 <input /> 在你清空欄位後馬上 focus<input />

var App = React.createClass({
  getInitialState: function() {
    return {userInput: ''};
  },
  handleKeyUp: function(e) {
    this.setState({userInput: this.getDOMNode().value});
    // this.setState({userInput: e.target.value}); # 官方範例使用 e.target.value 會導致無法正常運作。

  },
  clearAndFocusInput: function() {
    this.setState({userInput: ''}); // Clear the input

    // 我們希望在這邊可以 focus <input />


  },
  render: function() {
    return (
      <div>
        <div onClick={this.clearAndFocusInput}>
          Click To Focus and Reset
        </div>
        <input
          ref="theInput"
          value={this.state.userInput}
          onKeyUp={this.handleKeyUp}
        />
      </div>
    );
  }
});
React.renderComponent(<App />, document.getElementById('example'));

請注意,在這個範例我們想要通知 <input /> 做些事情。這件事件沒辦法透過 props 辦到。我們想要讓 <input /> focus ,然而這邊遇到了一點問題,那就是 render() 回傳的不是子元素 <input /> 的元件,在這個時候它只是一子元件的結構描述。
記得當你從 render() 回傳一個結構,它並不包含替你產生子元素的物件實例。關於在內部的標簽(內部的元件),它們都只是結構描述。
不過你可能已經想到把 <input /> 先存在變數裡了,注意!!你不應該把『這些事情或物件』存起來,然後期待這可能有一天會用到。如下

// 錯誤範例: DO NOT DO THIS!

render: function() {
  var myInput = <input />;          // 我可能會呼叫這個物件的 Method

  this.rememberThisInput = myInput; // 在未來某個時間點我就可以直接呼叫他

  return (
    <div>
      <div>...</div>
      {myInput}
    </div>
  );
}

在這個錯誤範例中,<input /> 只是描述結構。這個描述是用來建立 <input /> 背後的實際物件。
如果我們把這段程式完成如下並試著觀察這段程式碼的運作,事實上當你呼叫 this.rememberThisInput 是會出 ERROR 的!

<html>
  <head>
    <title>Hello React</title>
    <script src="http://fb.me/react-0.8.0.js"></script>
    <script src="http://fb.me/JSXTransformer-0.8.0.js"></script>
    <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <div>
      原生 Javascript 範例示範 focus 所以我們可以透過 element.focus() 讓元素 focus。
      <div id='test-trigger'>Click!</div>
      <input type='text' id='test' />
    </div>
    <script>
    /* 原生 Javascript 範例示範 focus */
    var HandleClick = function (e) {
      var el = document.getElementById('test');
      el.focus();
    };
    var el = document.getElementById('test-trigger');
    if (el.addEventListener) {
      el.addEventListener('click', HandleClick, false);
    } else {
      el.attachEvent('onclick', HandleClick);
    }
    </script>

    <script type="text/jsx">
    /** @jsx React.DOM */
    var App = React.createClass({
      getInitialState: function () {
        return {userInput: ''}
      },
      handleKeyUp: function (e) {
        this.setState({userInput: this.getDOMNode().value});
      },
      clearAndFocusInput: function () {
        this.setState({userInput: ''});
        // console.log(this.rememberThisInput);

        // this.rememberThisInput.focus();

      },
      render: function () {
        var myInput = <input value={this.state.userInput} onKeyUp={this.handleKeyUp} />
        this.rememberThisInput = myInput;
        return (
          <div>
            <div onClick={this.clearAndFocusInput} >Click to Focus and Reset</div>
            {myInput}
          </div>
        );
      }

    });
    React.renderComponent(<App />, document.getElementById('example'));
    </script>
  </body>
</html>

那我們怎麼通知 <input /> 背後的這個物件呢?

ref 屬性(attribute)

React 支援一個非常特別的屬性,你可以把它附加到任何在 render() 裡面的元件上(就是標簽 tag 上)。這個特殊的屬性可以讓你存取到對應的『背後的實際物件』,它保證可以在任何時間點存取到當下的物件。
下面是一個範例:
1 在 render() 裡將回傳任意的元素設定 ref 屬性(attribute)

<input ref='myInput' />

2 在程式碼(典型的範例是在處理事件或函式裡)中你就可以透過 this.refs 存取這個『背後的物件』。

this.refs.myInput

完整的範例

/** @jsx React.DOM */
var App = React.createClass({
  getInitialState: function () {
    return {userInput: ''};
  },
  handleKeyUp: function (e) {
    this.setState({userInput: this.getDOMNode().value});
    console.log(e); // 官方範例使用的 e.target.value 是錯誤的!


  },
  clearAndFocusInput: function (e) {
    this.setState({userInput: ''});
    this.refs.theInput.getDOMNode().focus();
  },
  render: function () {
    return (
      <div>
        <div onClick={this.clearAndFocusInput}>
          Click To Focus and Rest
        </div>
        <input ref='theInput'
               value={this.state.userInput}
               onKeyUp={this.handleKeyUp} />
      </div>
    );
  }
});
React.renderComponent(<App />, document.getElementById('example'));

在這個範例裡,render() 方法裡回傳的結構中包含了一個 <input /> 的結構描述,不過這次不同的是這個物件可以透過 this.refs.theInput 取得。然後在 clearAndFocusInput 函式裡使用 this.refs.theInput

總結

比起使用 this.propsthis.statethis.refs是操作物件或傳送訊息給特定子元素最方便的方式,但是建議不要透過他們去操作你的資料。一般來說被動處理計算的資料應該使用 this.propsthis.state

Refs 的用途

  • 可以定義任何 public 的 method 在你的元件類別裡(例如 reset 方法),然後透過 refs 去呼叫。
  • 在你需要呼叫 DOM 的 API 時取得該元素。this.refs.myInput.getDOMNode()
  • Refs 會自動記錄,如果你的子元素被破壞這個 refs 也會被破壞。不用擔心記憶體的問題,除非你做了一些瘋狂的事情,像是把整個物件都加入參考。

注意事項

  • 不要在 render() 方法裡面使用 this.refs 或者當任何元件的 render() 正在運行的時候。
  • 如果你想要使用 Google Closure Compiler (Javascript minify) ,請檢查不要用任何指定屬性屬性的方式使用 refs,這意味著當你設定了一個 ref='myRefstring'那麼你最好使用 this.refs['myRefString'] 這種方式。
  • 如果你是第一次用 React 開發,通常你會傾向讓 this.refs 去幫你達到你要的功能,如果遇到這種情形,請審慎思考關鍵在哪,該如何設計階層結構,該 state 管理控制哪些資料。 通常把state放置在元件最高階層,控制好關於『自己』的狀態會讓程式碼變得乾淨,清晰易懂。良好的設計 state 會導致你不會一直去使用 this.refs 強迫控制元件,且會讓資料易於控制和正確。

這篇文章為官方部落格的文章隻翻譯。原文

以觀念來說React 是一種使用 Javascript 來快速建立大型 Web 的方式,它非常容易擴展,且官方已將其使用在 Facebook 與 Instagram 上。

其中最好的部分就是 React 讓您在建立程式時重新思考關於應用程式。在這篇文章,將會引導您使用 React 完成一個可以搜尋過濾產品資料的範例。

從模擬架構開始

想像我們已經有了一個 JSON 的 API 以及一個設計師模擬的草圖。我們的設計師顯然不是很優,因為他的模擬像這樣:

而我們的 JSON API 傳回來的資料長得像這樣:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

第一步:拆解 UI 為元件階層結構

您即將要做的第一個步驟是根據模擬的 UI 畫出每一個元件(包含子元件)的階層方塊並且給予名稱,如果你正在與設計師一起工作那可能你已經完成這個任務。
看看他們的 Photoshop 中圖層的名稱大略就是你 React 元件最後的名稱。
不過我們怎麼知道哪個部分應該是元件?當您建立一個新的函式或物件,您可以根據單一職責原則,指的是每一個元件理想的情況下
應該只做一件事。如果該元件的功能不斷增加那就應該再把它拆解,並建立更小的子元件。

常見的需求是 - 顯示 JSON 的資料給使用者。根據過去的經驗,您會發現如果您的資料模型(Model)建立的正確,您的 UI (同時表示您的元件結構)就可以輕鬆的將資料呈現給使用者。
其原因是使用者界面和資料模型往往遵循一樣的資料結構,意味著其實將 UI 獨立為元件並非很困難。
就只是根據每一個小區塊需要呈現的資料模型去分解成個別的元件。

看到上圖,這個應用程式將會有 5 個元件,下面的斜體字表示每一個元件對應的模型

  1. FilterableProductTable (橘色) 用來組織包含其他子元件,即這個元件的最上層的容器。
  2. SearchBar (藍色) 取得 使用者輸入的搜尋條件。
  3. ProductTable (綠色) 根據 使用者輸入的搜尋條件 顯示過濾後的資料列表。
  4. ProductCategoryRow (青色) 顯示 分類 標題。
  5. ProductRow (紅色) 顯示每一個 產品

如果您認真觀察 ProductTable 你會看到表格還有標題列(即 NamePrice 欄位名稱那邊)並沒有被規劃為獨立元件。這只是偏好問題。
根據這個範例,我們規劃這個區塊為 ProductTable 的一部份,這是因為輸出產品列表資料是 ProductTable 的責任,當然包含欄位名稱,且目前看來它的工作很單純並不需要再拆出一個元件。
然而如果這個標題列變得越來越複雜(舉例來說:如果我們需要增加排序功能),如此一來增加一個 ProductTableHeader 元件會是比較好的做法。

現在我們已經定義好關於這個模擬的元件架構,讓我們重新組織成一個階層圖,這樣我們就能清楚看出元件的主從關係。

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:建立一個靜態版本的 React 元件

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        });
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." />
                <p>
                    <input type="checkbox" />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    render: function() {
        return (
            <div>
                <SearchBar />
                <ProductTable products={this.props.products} />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

現在您已經有了元件的階層結構。該是時候實作程式的功能了。
在學習 React 的過程中,我們推薦最簡單的方式是一開始只要建立一個能取得資料模型和渲染出 UI 畫面但是不能互動的版本。
拆成這些步驟是因為建立靜態版本通常是需要打很多字且不太需要思考,而建立互動機制需要您仔細的構思,如此一來在建立元件時比較不會出錯。

在建立靜態版本的時候通常你會思考關於元件 重複使用 的機會以及該怎麼透過 props 從父元素傳入資料。如果您已經熟悉關於 state 的觀念,就會知道在建立靜態版本時根本不應該使用 state
state 是互動時才會需要用到的功能,所謂的互動指的是當資料變動,而 UI 也需要對應更新。由於這只是靜態版本所以根本不需要用。

您可以由底層往上或者由上而下撰寫您的元件,意思是說你可以選擇從結構中最外層的元件開始建起(即從 FilterableProductTable)開始,或者從最內部的子元件開始(ProductRow)。
在單純的範例中,通常從上至下相對快速,而如果專案較大,通常從下而上會比較推薦,因為也同時方便您撰寫測試,可以逐步測試元件是否正常。

在這一步的最後,您會得到一個可重複使用元件的函式庫,你可以用它來輸出呈現你的資料模型,以確認 UI 的呈現是否有誤。
不過這個元件只有 render() 方法,因為截至目前為止它還只是靜態版本。

元件的最上層(FilterableProductTable)將會取得資料模型,透過屬性傳入資料。如果你修改了 Model 的資料且再次執行 renderComponent() 你應該會看到資料更新了。
這讓你可以清楚地觀察這個元件是怎麼更新資料,這就是 React 透過 one-way data flow (或稱 one-way binding) 單向數據流的方式去保持所有資料一致,同時也方便模組化。

簡易補充: props vs state

在 React 裏有兩種類型的 Model 就是你放資料的地方:propsstate。理解他們的區別非常重要,如果您還不懂他們之間的差別請閱讀官方或者這篇文章

第三步:定義最少但完整的 UI 狀態

為了讓您的 UI 俱有互動性,你可能會需要讓資料模型做些修改,接著 UI 根據 one-way binding 更新資料。
React 透過使用 state 讓這一切變得很簡單。您可以把 state 想是讓您存放動態資料的地方,而當資料有所變動,React 會自動呼叫 render() 執行 UI 的更新。

而要讓建立的程式能夠正確執行,首先需要思考關於這個程式最少需要哪些可變動的狀態,只有變動的資料才需要放到 state
關鍵的原則是 DRY (Don't Repeat Yourself) ,不重複原則。找出程式在特定需求內必須要的最少狀態。例如:如果你要建立一個 TODO List,其實你就只要一個陣列包含待辦清單的項目。
當你需要計算項目總數時,不需要在 state 中的儲存另一個變數,而是單純使用陣列取得數量即可。

思考我們這個範例中各種取得的資料

  • 所有產品的列表
  • Search input 的搜尋條件
  • checkbox 是否有被選取的值
  • 過濾後的清單

讓我們一個一個討論看看誰是屬於 state 。簡單的思考關於這三個問題

  1. 資料是透過 props 從父元素傳進來的嗎?如果是,這可能不屬於 state 。
  2. 這資料會隨著時間推移而改變嗎? 如果不是,那它應該不屬於 state 。
  3. 你能從現有任何 state 或者 props 計算出這個資料嗎?如過是!那這肯定不屬於 staet。

產品列表是透過 props 傳遞進來的,所以這不應該存在 state,當然有經驗的開發者會說這通常從資料庫來,但這個範例不是。
搜尋條件和 checkbox 似乎是狀態,因為他們會改變。而且是不能透過計算得到的。
最後過濾後的清單也不該儲存在 state ,因為他是可以被計算出來的,根據我們拿到的過濾條件去運算。
所以最後我們歸納出應該被放在 state 的有:

  • 搜尋條件
  • checkbox 的值

第四步:應該在何處使用 state

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." value={this.props.filterText} />
                <p>
                    <input type="checkbox" value={this.props.inStockOnly} />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

OK 我們已經決定了這個元件程式最少的 state,下一步我們需要定義哪些元件是要變動的,以及是哪些要使用 state
記住!React 提供的是一種單向資料流的結構,通常是由上而下。剎那間可能不太輕易判斷哪個元件該管理或使用 state,這通常也是初學者最難理解的部分。
請跟著下面這些規則去推敲:

思考程式中需要使用到 state 的部分

  • 找出哪些元件需要根據 state 輸出不同的結果
  • 找出共同的擁有者元件(最上層的元件通常需要管理 state)
  • 如果你不能找出某一個元件該擁有狀態的理由,那就建立一個新的元件用來管理狀態,並且加在共同擁有者元件之上。

讓我們應用這些規則在這個範例上

  • ProductTable 需要根據 state 過濾產品列表以及 SearchBar 需要顯示搜尋條件的值和 checkbox 的狀態。
  • 共同擁有者元件是 FilterableProductTable
  • 把過濾條件和 checkbox 值都放在 FilterableProductTable 在概念上也是合理的。

所以我們決定 state 應該放在 FilterableProductTable ,首先加上 getInitialState() 方法,讓它回傳一個物件 {filterText: '', inStockOnly: false}
這是用來初始化 state 的,接著傳入 filterTextinStockOnlySearchBar 當作屬性,最後在 ProductTable 中使用這些屬性值去過濾,並且設定 form 的值。

現在你可以看到您的應用程式俱有這些行為:在 state 中把 filterText 的值設成 ball 然後資料就會更新。

註:先別急著操作網頁上的 form。

第五步:加入反向數據流

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        console.log(this.props);
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    handleChange: function() {
        this.props.onUserInput(
            this.refs.filterTextInput.getDOMNode().value,
            this.refs.inStockOnlyInput.getDOMNode().checked
        );
    },
    render: function() {
        return (
            <form>
                <input
                    type="text"
                    placeholder="Search..."
                    value={this.props.filterText}
                    ref="filterTextInput"
                    onChange={this.handleChange}
                />
                <p>
                    <input
                        type="checkbox"
                        value={this.props.inStockOnly}
                        ref="inStockOnlyInput"
                        onChange={this.handleChange}
                    />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    handleUserInput: function(filterText, inStockOnly) {
        this.setState({
            filterText: filterText,
            inStockOnly: inStockOnly
        });
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                    onUserInput={this.handleUserInput}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

到上面為止你會發現除非你手動更改 state 的設定,如果你在網頁的表單上輸入任何東西,input 完全沒反應。這是因為 React 是單向數據流的模式。
現在讓我們補上其他方向來的數據,由於表單元件在這個結構的內部,而我們只能用 FilterableProductTable 去更新 state
React 使得數據流非常明確,清楚易懂,歸納的結論就是更新 state 和資料操作請在 owner 擁有者元件裡作,而當子元件的觸發的行為需要更新數據時還是拿父元件的方法。
不過這個方式的缺點就是你需要多打一些字,相較于 two-way binding。雖然 React 也提供一個擴充套件叫做 ReactLink 它可以協助您快速做到 two-way binding,不過這篇文章是用來說明整個
React 基礎的觀念,所以我們不打算在這篇提太多額外的東西以免造成混淆。

如果您是著輸入一些條件或者勾起 checkbox 在這上一版的程式碼,您會看到 React 忽略您的輸入。這是故意的,因為我們已經設定 input 的 value 是 this.state.filterText ,他就要確保永遠等於這個參考
讓我們來想想我們希望怎樣,我們希望確保使用者輸入的任何改變都是去更新 state ,而 input 則一樣從 state 取得資料。

FilterableProductTable 就要把修改 state 的函式傳給 SearchBar,如此一來當 input 觸發 onChange 時才能變更 state。
雖然這樣聽起來好像會多了不少程式碼,但這能確保資料流向是非常清楚的。

最後,就這樣而已

希望這篇文章能夠使您理解關於 React 如何建立元件和應用程式的觀念。
雖然它比起你現在的框架或程式碼的確讓你多打了一些字,不過記住讀程式碼遠遠比撰寫還要困難,而這麼做會讓你的程式碼模組化且非常容易閱讀。

在 codepen.io 上使用 React

為了能夠在 CodePen 上使用 React 和 JSX 您必須要:

  1. 加入這支 script 到 CodePen http://codepen.io/chriscoyier/pen/yIgqi.js
  2. React: http://fb.me/react-0.11.1.js
  3. JSX Transformer: http://fb.me/JSXTransformer-0.11.0.js


緣由

React 是一個由 Facebook 團隊所提供的一組 Javascript 函式庫。
當您開始使用 CodePen 撰寫一些 React 範例時會發現 CodePen 無法正常運作。
這是因為當您在 Javascript 區塊輸入程式碼時,他其實只是在您的文件上加上一個
<script> 標簽且並沒有定義任何 type
所以當您想使用 CodePen 轉寫一些小範例時您有幾種選擇:

  1. 不使用 JSX,使用類似像 React.DOM.div 之類的原生 JS 取代
  2. 將 JS 寫在 html 區塊,並且使用 <script type='text/jsx'>
  3. 使用上述的方式加入一段 script

關於上面這一小段程式碼是由Mark Funk所提出的一個解法。

RunScript.js
(function() {
  function runScripts() {
    var bodyScripts = 'body script:not([src])';
    Array.prototype.forEach.call(document.querySelectorAll(bodyScripts), function setJSXType(element) {
      element.setAttribute('type', 'text/jsx');
    });
  };

  if (window.addEventListener) {
    window.addEventListener('DOMContentLoaded', runScripts, false);
  } else {
    window.attachEvent('onload', runScripts);
  }
})();

簡單來說這段程式碼會尋找 CodePen 放置到預覽中的 script 標簽並且加入 type
附帶一提的是,JSX Transformer 是用來協助您方便開發的並不適用于發佈的產品上。
最後,當您發生錯誤時請檢查您瀏覽器的 console,JSX Transformer 會很貼心的
提示您錯誤訊息。

React 揭秘

關於這篇文章將會試著解釋關於 React 核心的概念。

鳥瞰架構

在傳統的網頁應用程式中,我們如果要增加互動性時勢必廣泛的操作 DOM 元素,一般來說現在最普遍的技術是使用 jQuery:


上圖我們故意讓 DOM 示意為紅色這是因為操作更新 DOM 是需要付出昂貴的代價,也意味著這很吃效能。
很多時候我們會使用 Model 來記錄關於 APP 狀態,不過通常我們最後目標是必須要將狀態呈現給使用者,所以我們必須自己實作這些細節。
這已經是我們很稀鬆平常的開發模式。

而 React 的主要目標就是提供一種不同且更有效率的方式去執行關於操作更新 DOM 這個部分,最終這個方式會取代我們直接操作 DOM 的方法。
React 使用的方式是透過建立一套虛擬 DOM 的機制,React 幫你處理關於操作 DOM 方面的事情。

為什麼多引進一層架構會讓效能增加? 如果在其架構之上多引入一層可以提升速度,這不是暗示瀏覽器並沒有實作最佳的 DOM 操作方式。
這也意味著虛擬 DOM 有著跟實際 DOM 不同的語義和行為。值得關注的是當我們改變虛擬 DOM 時並不能保證立即得到效果。
也因為這個機制導致 React 在實際接觸 DOM 之前必須要等待事件回圈結束。在同一時間它會去計算最小差異並盡可能的用最少的步驟去更新 DOM。

如此一來應用程式便能獨立執行批次更新,套用計算後的差異到實際 DOM 上,任何應用程式如果這麼做那麼都能夠像 React 一樣有效率。
但實際上自己編寫程式碼去做這些任務是很繁瑣且容易出錯,React 的精華之處就是幫你處理掉這些問題。

元件

就上面所提到的虛擬 DOM 的機制有著跟直接操作實際 DOM 不一樣的語義和行為,所以也會有明顯不同的 API。
所謂的元素即在 DOM 結構中的一個節點(node),不過在虛擬 DOM 機制底下一個節點完全是不一樣的東西,我們稱這個節點為元件

使用元件對 React 來說是一件非常重要的事情,因為元件的設計概念是要拿來做計算的,就是計算和實際 DOM 的差異。
比起計算整個結構的差異,React 透過虛擬 DOM 將使得實際執行的時間複雜度大幅下降。

為了理解為什麼? 我們必須深入探討元件的設計,就從 Hello World 範例:

/** @jsx React.DOM */
var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

React.renderComponent(<HelloMessage name="Andy" />, mountNode);

上面這段程式碼出現了一些可怕的東西,且在這個階段無法完全說明清楚。即使是這麼小的一段範例都包含著一個很強大的概念,所以在這邊我們將會花些時間慢慢一點一點說明。

這個範例建立了一個 React 元件的類別(class): HelloMessage,然後透過 renderComponent() 在虛擬的 DOM 的機制中建立一個元件(<HelloMessage />, 本質上它就是 HelloMessage 類別實例化的物件,同時也是一個虛擬的 DOM)
最後把這個物件裝到真實的 DOM 元素(mountNode)。

首先是需要注意的事情是 React 的虛擬 DOM 通常來自您在應用程式中客制的元件(在這個例子是 <HelloMessage>)。這是一個意義重大的新嘗試,從內建的 DOM 分離出來。
DOM 通常不帶有任何程式邏輯,就只是一個被動的資料結構,且讓我們能夠附加處理事件。換句話說 React 的虛擬 DOM 是透過特定程式中的元件所創造的,且能夠加入程式中的特定 API 及內部邏輯。
這樣的方式比起直接修改操作 DOM ,例如: 使用 jQuery 的方式,這種建置 View 的方法是一種全新的抽象化方式與框架。

值得一提的是: 如果您一直持續關注 HTML 你也許知道關於 HTML 也許很快的也能自訂 DOM
這將會帶給 DOM 類似的功能: 定義特定程式使用的 DOM 元素,不過 React 並不需要等到官方和瀏覽器完全實作這件事,因為虛擬 DOM 並不是真的 DOM。這讓 React 搶先在自訂元素與 Shadow DOM
這些功能實作普及之前您就能先用了。

回到我們的範例,我們已經建立了一個叫做 <HelloMessage> 的元件並且掛載 mountNode 裡面。
讓我們用圖片來說明初始化幾個部分的情形,首先,我們將虛擬 DOM 與實際 DOM 的關係視覺化,假設 mountNode 是網頁中的 <body> 標簽:

關於掛載(mount)一詞就理解為『對應』,把 a 掛載到 b 上 = 可以把 b 視為 a 。

箭頭表示虛擬元件已經被掛載到原生 DOM 元素中。這段過程非常短,不過也讓我們來看看關於應用程式的視圖部分的邏輯:

這張圖片指的是整個網頁的內容是由我們客制的 <HelloMessage/> 來呈現,那麼關於 <HelloMessage/> 看起來到底長怎樣?

關於元件輸出渲染的部分是透過 render() 去定義欲呈現的元素。React 並沒有確切的說明關於何時或多頻繁的會去執行 render()
只有告訴我們當它注意到有效合法的改變時本身會去執行足夠次數的 render(),無論你回傳什麼樣的 DOM 結構。

在我們這個案例,render() 回傳了一個 <div> 裡面包含了一些內容。React 會執行這個 render() 取得 <div> 然後更新實際的 DOM ,使兩者一致。

不僅僅是更新 DOM,還會幫你記住已經更新的東西。這也是我們待會會提到的關於 React 如何快速的判斷其中差異的方法。
關於 render() 如何回傳 DOM 節點,這邊我先簡略帶過。它是透過 JSX 去定義結構,這是一個非原生的 Javascript,注意雖然它看起來像是 XML。
最後 JSX 會被編譯回 Javascript,看看 JSX 的編譯結果有助我們理解這個架構:

/** @jsx React.DOM */
var HelloMessage = React.createClass({displayName: 'HelloMessage',
  render: function() {
    return React.DOM.div(null, "Hello ", this.props.name);
  }
});

React.renderComponent(HelloMessage( {name:"John"} ), mountNode);

看到了吧!我們真的不是回傳一個 DOM 元素,而是一個等價于 DOM 元素的 React Shadow DOM。所以我們得知 React 回傳的並不是真的 DOM。
您可以理解為標記物件。

狀態與變化

到目前為止,我們忽略了故事中很重要的一段,關於元件可以被改變這件事。如果一個元件不允許被調整修改,那 React 跟 static rendering framework(靜態渲染框架) 也沒啥兩樣,功能就類似于 Mustache 或者 HandlebarsJS
這些樣板引擎,不過 React 的重點就是有效率的更新,要能夠更新元件勢必要允許我們修改一些狀態之類的東西。

React 使用元件的 state 屬性來表示其狀態的資料模型。
關於這點在官方文件的第二個範例就有舉例說明:

/** @jsx React.DOM */
var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});

React.renderComponent(<Timer />, mountNode);

React 會在適當的時間點執行回呼函式 getinitialState(), componentDidMount() 以及 componentWillUnmount() ,根據到目前為止的解釋您應該可以清楚地理解這些函式名稱的其含義。
所以我們推測元件和狀態背地裡的行為:

  1. render()stateprops 的一個 function,也就是當它們發現異動會執行 render。
  2. state 只能透過 setState() 去改變。
  3. props 不應該持續變動,只有當其父元素用新的屬性重新輸出時才改變。

(在這之前我們沒有明確地提到 props ,不過他們就是屬性 attributes。當元件要 render 時,它們就來自那些 JSX Tag 中的屬性)

稍早,我們曾經提到 React 會自己執行足夠次數的 render ,意思是除非有需要不然 React 不會執行 render。
那需要什麼?當你發動 setState() 或者父元件重新賦予新的 props,React 就會重新輸出。

現在我們將所有的事情放在一起,用一張圖來說明當程式更新了虛擬 DOM 的資料流(例如: 回應 AJAX 呼叫):

  1. 發動了 AJAX
  2. React 內部需要呼叫 setState 用以改變內部狀態
  3. 因內部狀態改變進而觸發 render
  4. render 執行需要依照生命週期呼叫像 componentWillMount 這類的方法
  5. 最後根據計算的最小差異更新 DOM

從 DOM 取得資料

截至目前為止,我們只有討論到關於收到狀態改變,到如何傳遞到實際 DOM,但實務上我們會需要從 DOM 取得資料,例如從 input 取得使用者輸入的資料。
為了觀察如何運作,我們取用了官方第三個範例:

/** @jsx React.DOM */
var TodoList = React.createClass({
  render: function() {
    var createItem = function(itemText) {
      return <li>{itemText}</li>;
    };
    return <ul>{this.props.items.map(createItem)}</ul>;
  }
});
var TodoApp = React.createClass({
  getInitialState: function() {
    return {items: [], text: ''};
  },
  onChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var nextItems = this.state.items.concat([this.state.text]);
    var nextText = '';
    this.setState({items: nextItems, text: nextText});
  },
  render: function() {
    return (
      <div>
        >h3<TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.onChange} value={this.state.text} />
          <button>{'Add #' + (this.state.items.length + 1)}</button>
        </form>
      </div>
    );
  }
});
React.renderComponent(<TodoApp />, mountNode);

簡單的說,我們把我們要的行為綁定到 DOM 的事件(在這個範例就是 onChange),接著你在這個事件中呼叫 setState 讓 React 幫您更新介面(實際的 DOM)。
如果您的程式有資料模型,您的事件大概就是透過 setState 更新那個資料模型,React 發現狀態異動就會去更新。
另外如果您曾經用過其他提供雙向資料繫結的框架,看起來可能會懷疑 React 本身在技術上退化了?

儘管這個範例看起來 React 並不是真的把事件加到 <input> 的 "onChange" ,取而代之的是加到文檔層級。讓事件透過汽泡傳遞的機制然後分派他們到正確的虛擬 DOM 元素。
這麼做的好處是包含提升速度(在 DOM 綁定太多處理事件會讓網站變慢),跨瀏覽器實現一樣的行為(處理事件的屬性和其派送的行為並沒有統一的標準,意思是在不同瀏覽器可能有些許的差異)。

所以最後我們可以總結一張完整的圖片來說明關於資料流和事件處理機制:

結論

  • React 是一個處理 View 的函式庫: React 並不強迫您要在 Model 做些什麼設定或改變。一個 React 元件只是一個 View-Level 的概念,而元件的狀態就只是 UI 方面的狀態。您可以繫結任何類型的資料模型或者函式庫到 React(雖然某些資料模型的處理方式會更有效率,例如 Om)
  • React 的元件抽象化在更新 DOM 的方面尤其優秀: 元件抽象化是一個原則,使得我們可以編寫組織良好的架構,同時又提供高效率的更新機制。
  • React 元件從 DOM 的角度執行更新不太方便: 比起函式庫自動傳遞同步資料模型,撰寫事件處理給 React 帶來一種很低階的感覺。
  • React 是抽象漏洞: 意味著 React 有本質上的缺陷,但有提供避免問題發生的方向。大部份的時間你的程式只會和虛擬 DOM 打交道,但有時候你需要直接對 DOM 做些操作。此時您可以查閱手冊的這部分

實作一個 Tabs 元件

複合式(組合)元件

在 React 中任何東西都是元件,就像樂高一樣,你可以用小片的積木組成大塊的,再組合出您想到的東西。
同樣的道理您也可以用許多的小元件(小功能模組)來組合出您的應用程式。所謂的複合式元件或稱作組合元件,
他其實就是由多個元件去組成一個多功能的大元件。

在這篇文章我們要來建立一個 tabs 標簽切換功能的元件,為了達成這個功能我們需要 4 個不同的元件:
<Tabs />, <TabList />, <Tab /><TabPanel /> 分別用來呈現整個 tabs , 列出
標簽列,標簽列的按鈕,以及顯示的內容。結構如下:

 <Tabs>
  <TabList>
    <Tab>Iron man</Tab>
    <Tab>Superman</Tab>
    <Tab>Lucy</Tab>
  </TabList>
  <TabPanel>
    鋼鐵人介紹
  </TabPanel>

  <TabPanel>
    超人介紹
  </TabPanel>

  <TabPanel>
    鹿茸介紹
  </TabPanel>
 </Tabs>

首先是 <Tabs/> 的行為,它被用來當做一個容器,其角色有點像是一個 controller ,因為它必須要掌管所有 DOM 的事件(點擊 Tab 切換至該內容,被選取到的 index)
同時也需要管理 state 看看哪個 <Tab/> 目前正被選取到,所以我們會稱 <Tabs /> 為擁有者元件(owner component)。

我們遭遇到的第一個挑戰是: 元件之間該如何溝通。每一個元件都有一個 state 。每當 state 發生變動,React 就會更新並重新渲染元件以使其跟 state 一致。
當我們選了某個索引後,<Tabs/> 元件就要去更新 <Tab/><TabPanel/>state

典範轉移

首先我們為每一個元件建立一個 API,透過建立一個方法來變更 state。在 React 中一個元件可以透過 this.props.children 去存取子元件。
所以一開始我們理論上只要使用 handleSelected 去設定適當該顯示的子元件如下:

var tabs = this.props.children[0].props.children,
    panels = this.props.children.slice(1),
    index = this.state.selectedIndex;
tabs[index].handleSelected(true);
panels[index].handleSelected(true);

這樣的做法在 v0.10.0 以前的版本是可以運作的,概略的實作如下:

/**
 * @jsx React.DOM
 */


var Tab = React.createClass({
  getInitialState: function () {
    return {selected: false}
  },
  handleSelected: function (status) {
    this.setState({selected: status});
  },
  render: function () {
    var cx = React.addons.classSet;
    var classes = cx({
      'react-tab': true,
      'active': this.state.selected
    });
    return (
      <li className={classes}>
        <a href='#' data-index={this.props.index}>{this.props.children}</a>
      </li>
    );
  }
});

var TabList = React.createClass({
  render: function () {
    return (
      <ul className='react-tab-list'>
        {this.props.children}
      </ul>
    );
  }
});

var Tabs = React.createClass({
  getInitialState: function () {
    return {selectedIndex: 0}
  },
  componentDidMount: function () {
    var tabs = this.props.children[0].props.children,
        panels = this.props.children.slice(1),
        index = this.state.selectedIndex;
    for (i in tabs) {
      if (i == index) {
        tabs[i].handleSelected(true);
        panels[i].handleSelected(true);
      } else {
        tabs[i].handleSelected(false);
        panels[i].handleSelected(false);
      }
    }
  },
  handleClick: function (e) {
    var index = parseInt(e.target.getAttribute('data-index'));
    var tabs = this.props.children[0].props.children,
        panels = this.props.children.slice(1);
    for (i in tabs) {
      if (i == index) {
        tabs[i].handleSelected(true);
        panels[i].handleSelected(true);
      } else {
        tabs[i].handleSelected(false);
        panels[i].handleSelected(false);
      }
    }

  },
  render: function () {
    return (
      <div className='react-tabs' onClick={this.handleClick}>
        {this.props.children}
      </div>
    );
  }
});

var TabPanel = React.createClass({
  getInitialState: function () {
    return {selected: false}
  },
  handleSelected: function (status) {
    console.log(this.props.name + ' selected: ' + status);
    this.setState({selected: status});
  },
  render: function () {
    var cx = React.addons.classSet;
    var classes = cx({
      'react-tab-panel': true,
      'active': this.state.selected
    });
    return (
      <div className={classes}>
        {this.props.children}
      </div>
    )
  }
});

var App = React.createClass({
  render: function () {
    return (
      <div className='container'>
        <Tabs>
          <TabList>
            <Tab name='ironman' index={0}>Iron man</Tab>
            <Tab name='superman' index={1}>Superman</Tab>
            <Tab name='lucy' index={2}>Lucy</Tab>
          </TabList>

          <TabPanel name='panel-ironman'>
          鋼鐵人
          </TabPanel>

          <TabPanel name='panel-superman'>
          超人再起
          </TabPanel>

          <TabPanel name='panel-lucy'>
          露西
          </TabPanel>
        </Tabs>
      </div>
    );
  }
});

React.renderComponent(
  <App />,
  document.getElementById('example')
);

See the Pen oitzv by AndyYou (@AndyYou) on CodePen.

不過到了 React v0.10.0 版本的時候這樣做會出現警告:
Invalid access to component property "setSelected"


到了 v0.11.0 的時候更慘您已經無法直接存取子元件的方法,因為是新版的 React this.props.children 回傳的物件只是描述物件(discriptors)。不再是對應元件的參考物件,且官方建議您不應該直接存取子元件的實際物件。

Component Refs

React 提供一種機制給你取得實際元件的物件,就是使用 refs

var App = React.createClass({
  handleClick: function () {
    alert(this.refs.myInput.getDOMNode().value);
  },

  render: function () {
    return (
      <div>
          <input ref="myInput"/>
          <button
              onClick={this.handleClick}>
              Submit
          </button>
      </div>
    );
  }
});

透過 refs 屬性您就可以取得該子元件的參考

動態的子元件

典型的 refs 使用方式是父元件已經知道子元件的情況,所以可以直接在 tag 中指定 ref 如上面的範例。
不過這次我們希望我們的 Tabs 元件可以動態的放入 <Tab/><TabPanel/>
例如:

App.js
var App = React.createClass({
  render: function () {
    return (
      <div className='container'>
        <Tabs>
          <TabList>
            <Tab name='ironman' >Iron man</Tab>
            <Tab name='superman' >Superman</Tab>
            <Tab name='lucy' >Lucy</Tab>
          </TabList>

          <TabPanel name='panel-ironman'>
          鋼鐵人
          </TabPanel>

          <TabPanel name='panel-superman'>
          超人再起
          </TabPanel>

          <TabPanel name='panel-lucy'>
          露西
          </TabPanel>
        </Tabs>
      </div>
    );
  }
});

而 Tabs 只是動態地把開發者加入的任意結構輸出

Tabs.js
var Tabs = React.createClass({
  render: function () {
    return (
      <div className='react-tabs'>
        {this.props.children}
      </div>
    );
  }
});

因此我們需要一些動態指定 refs 的方法,而這個方法就是透過 cloneWithProps

var App = React.createClass({
  render: function () {
    var index = 0,
        children = React.Children.map(this.props.children, function (child) {
        return React.addons.cloneWithProps(child, {
            ref: 'child-' + (index++)
        });
    });

    return (
        <div>
            {children}
        </div>
    );
  }
});