元素(elements)是 React 應用程式中最小的單元,元素中會包含其外觀的描述:
const element = <h1>Hello, world</h1>;
React 的元素屬於靜態的物件,所以在建立時非常有效率,而 React 自己會自動維護與瀏覽器 DOM 之間的同步問題。
通常在 React 的應用程式中,我們會在網頁上放置一個類似這樣的 HTML 標籤,然後讓 React 的所有元件都顯示在這個標籤之中:
<div id="root"></div>
這樣的 HTML 節點就稱為根節點(root),通常一個應用程式中只有一個根節點,但是如果我們是將 React 整合進既有的程式當中,我們也可以加上任意數量的根節點。
若要顯示 React 的元素,可以呼叫 ReactDOM.render()
函數,並指定要顯示的 React 元素,以及放置元素的根節點:
const element = <h1>Hello, world</h1>; ReactDOM.render(element, document.getElementById('root'));
上面這段程式碼執行之後,就會在網頁上顯示「Hello, world」的字樣。
React 的元素是不可變更的(immutable),也就是說當我們建立好一個 React 的元素之後,就不可以更改其屬性或是子節點,它就像是電影中的一幅畫面,代表某個時間點的 UI 狀態。
以目前我們所學到的技巧來說,如果想要更新 UI,只能重新建立一個 React 元素,然後交給 ReactDOM.render()
再畫一次。
以下是一個時鐘範例:
// 更新 UI function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render(element, document.getElementById('root')); } // 每秒更新一次 setInterval(tick, 1000);
這個範例中,我們每秒呼叫一次 tick()
更新 UI,這樣就可以達到動態時鐘的效果。
ReactDOM.render()
函數一次,配合 React 的狀態功能來更新 UI(後續會介紹)。在概念上來說,React 的組件(components)就像是 JavaScript 的函數,它可以接受任意的輸入(稱為 props,意指屬性),並傳回要顯示於網頁中的 React 元素。
組件最簡單的定義方式就是使用 JavaScript 的函數:
// 以 JavaScript 函數定義組件 function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
這一個 JavaScript 函數是一個有效的 React 組件,它接受單一個 props
參數,並傳回一個 React 元素。
除了 JavaScript 函數之外,亦可使用 ES6 的 class 定義 React 的組件:
// 以 ES6 class 定義組件 class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
以上兩種組件定義方式,對於 React 來說是完全一樣的。
前面我們只看過標準的 DOM 標籤元素:
// DOM 標籤元素 const element = <div />;
事實上在 React 中,我們也可以自行定義新的組件:
// 自訂組件 const element = <Welcome name="Sara" />;
當 React 遇到使用者自行定義新的組件時,它會將 JSX 的所有屬性打包成一個物件,透過 props 傳入該組件。
以下這個範例會在網頁上產生「Hello, Sara」的字樣:
// 自訂組件 function Welcome(props) { return <h1>Hello, {props.name}</h1>; } // 使用自訂組件,並傳入 props 屬性資料 const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );
這裡當 ReactDOM.render()
遇到 Welcome
組件時,就會呼叫 Welcome
函數,並將 JSX 的屬性打包為 {name: 'Sara'}
,作為 props
的值傳入,然後在 Welcome
函數中就可以從 props
中獲取該屬性值,產生正確的 React 元素。
<div />
;而若遇到大寫開頭的組件,則視為自訂組件(必須自己定義好),例如 <Welcome />
。一個組件的輸出中可以包含其他的組件,這種特性讓我們可以將網頁應用程式上各層級的元件統一都以組件的方式來表達。
以下的範例中,我們建立一個組件,其中包含三個 Welcome
組件:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } // 自訂組件,包含其他組件 function App() { return ( <div> <Welcome name="Sara" /> <Welcome name="Cahal" /> <Welcome name="Edite" /> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
通常新的 React 應用程式都會從一個最頂端的 <App>
組件開始打造起,但若是將 React 整合進既有的應用程式中,可能就會從最基本的零組件開始著手(例如 <Button>
)。
在使用 React 開發網頁應用程式時,應該進可能將重複的部份獨立出來,寫成組件,可讓程式碼更簡潔。以下是一個簡單的範例:
function Comment(props) { return ( <div className="Comment"> <div className="UserInfo"> <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name} /> <div className="UserInfo-name"> {props.author.name} </div> </div> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
這段程式碼非常冗長,結構也複雜,不容易維護,也不容易重複使用。
首先將這裡的 Avatar
獨立出來,寫成組件:
function Avatar(props) { return ( <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> ); }
由於 Avatar
並不一定都顯示在 Comment
當中,所以其人名的屬性以 name
命名,不用 author
。
這樣一來,Comment
就變得簡潔一些了:
function Comment(props) { return ( <div className="Comment"> <div className="UserInfo"> <Avatar user={props.author} /> <div className="UserInfo-name"> {props.author.name} </div> </div> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
接著再將 UserInfo
也獨立出來:
function UserInfo(props) { return ( <div className="UserInfo"> <Avatar user={props.user} /> <div className="UserInfo-name"> {props.user.name} </div> </div> ); }
最後完成的 Comment
為:
function Comment(props) { return ( <div className="Comment"> <UserInfo user={props.author} /> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
萃取元件的大原則有兩個:
若程式碼符合上述任一種特性,就可以考慮將其獨立出來,寫成組件。
不管是以 JavaScript 函數或是 ES6 class 來定義組件,組件 props 中的值都是不可以更改的。例如:
// 沒問題 function sum(a, b) { return a + b; }
上面這個範例中,我們使用 a
與 b
來做計算,但是沒有更改 a
與 b
裡面的內容,所以這樣是沒問題的。
但以下這樣寫,就會出問題:
// 不可以這樣寫! function withdraw(account, amount) { account.total -= amount; }
這個 withdraw
函數在執行時,會更改 account.total
內所儲存的數值,這在 React 中是不允許的。
如果 UI 需要隨時間、使用者的操作等改變其內部的狀態以及顯示的外貌,可以使用接下來要介紹的 state 功能。