"本文章附有影片"。這個程式最後的呈現結果,會是在網頁上出現一個文字輸入框,當你輸入文字時,下面的顯示區域會一併跟著顯示,就像下面的動態圖片這樣:
註: 本文章附有影片,影片網址在Youtube的這個網址。本文章同步放置於Github庫的這裡,所有的程式碼也在裡面。
我們在這個範例程式中,要使用四個程式碼檔案,其中二個與之前的幾乎是一樣的。
第一個是無狀態的顯示元件,名稱是TextShow,它的檔案名稱是TextShow.js,裡面的內容很簡單,只有下面這樣而已:
components/TextShow.js
// @flow
import React from 'react'
const TextShow = (props: { text: string }) => (
<h1>{props.text}</h1>
)
// 加入props的資料類型驗証
TextShow.propTypes = {
text: React.PropTypes.string.isRequired
}
// 匯出TextShow模組
export default TextShow
你可以看到這支程式最上面已經有加了// @flow
的註記,這代表它會被Flow工具進行檢查,Flow工具有一些與React特別搭配的檢查工具,其中它會檢查props
在這種函式元件語法中,在傳入值時是什麼樣的物件類型結構,因為我們的TextShow只有一個text屬性會由上層元件傳入,所以用props: { text: string }
來指定傳入的props的型態。
另一個檔案是我們主要的元件名稱是TextInput,而且它裡面會匯入使用到TextShow元件,檔名一樣是用TextInput.js
,因為檔案滿長的,所以分幾個部份來看首先是最前面的部份:
components/TextInput.js
// @flow
import React from 'react'
import TextShow from './TextShow'
type Props = {
initText: string,
}
class TextInput extends React.Component {
state: {
text: string,
}
// 建構式
constructor(props: Props) {
// super是呼叫上層父類別的建構式
super(props)
// 設定初始的狀態。注意!這裡有個反樣式,不要用props的值來設定state的初始值
this.state = {
text: '',
}
}
//後面還有程式碼...
}
這支程式的最上面也有加了//@flow
的註記,它會對React中的一些特性作檢查,一個是props,另一個是state這兩個物件的結構。你可以注意到它們的設定方式不太一樣,這個部份的程式展示了建構式的寫法,建構式區塊中的第一行,通常都是使用super()
來呼叫繼承的父類別。
// 處理的方法,用e.target可以獲取到輸入框的值,用箭頭函式可以綁定`this`
handleChange = (e: Event) => {
// Flow會檢查必定要HTMLInputElement的物件才能有輸入值
if (e.target instanceof HTMLInputElement) {
this.setState({text: e.target.value})
}
}
// 渲染方法,回傳React Element(元素)
render() {
return (
<div>
<input type="text"
value={this.state.text}
placeholder={this.props.initText}
onChange={this.handleChange}
/>
<TextShow text={this.state.text}/>
</div>
)
}
這個接下來的程式碼部份,是這個範例的重點。首先這使用了之前說明的在類別中的箭頭函式,這是一個超出ES6標準的語法。這是為了要綁定this在這個方法中可以使用,因為在這個方法中,我們需要使用到this.setState
這個方法。
setState
是一個元件中非常重要的方法,state(狀態)是元件的內部使用的一個值(一般為物件值),在元件中的state是不可以直接更動其中所包含的值,也就是如果你直接用this.state.text = 'Hello'
這樣的更動語句,React會產生錯誤給你。唯一可以更動state的,只有透過setState
這個方法,這是React中我們學到的第二條強硬規則。
特別注意: 唯一可以更動元件中的state(狀態),只有使用setState方法。
這個handleChange
方法主要是要處理文字框輸入的事件,它可以獲取得到事件物件,在React中使用JSX語法所寫的事件都是人造(假的)事件,是由React中設計出來對應真實DOM中的事件物件,實際在使用時與一般的事件差不多。React中的事件的對應,類似於DOM事件處理模型的內聯模型的寫法,是寫在JSX語法中對應的DOM元件標記上的。也就是對應下面的這段在render
方法中onChange
的語法:
<input type="text"
value={this.state.text}
placeholder={this.props.initText}
onChange={this.handleChange}
/>
React中的人造事件,都是使用小駝峰的命名法,例如onChange
、onClick
這樣,而DOM事件處理內聯模型是用全小寫,React會協助處理用監聽的方式來處理事件,你不需要再使用addEventListener
的方法來作事件的監聽。關於React中處理事件的方式,你可以參考官方的這篇文件中的說明。
第三個檔案是App.js,它仍然是最上層的用於集中所有元件的元件,與之前的範例相同。它會設定TextInput中的props.initText這個屬性值。程式碼如下:
components/App.js
import React from 'react'
import TextInput from './TextInput'
class App extends React.Component {
render() {
return <TextInput initText="開始輸入文字吧!" />
}
}
//輸出App元件
export default App
第四個檔案是我們用來呈現元件在網頁上的程式碼檔案index.js,裡面會引用TextInput元件,程式碼如下:
index.js
import React from 'react'
import { render } from 'react-dom'
import App from './components/App'
render(
<App />,
document.getElementById('root')
)
props(屬性)並沒有限制你只能指定給它什麼樣類型的值,可以是JavaScript中常見的幾種資料類型,字串、數字、布林、函式、陣列、物件與符號(Symbol)。你從範例中看到,元件會自動就會取得你所定義的props(屬性)值,例上面的範例中TextShow
元件中的程式碼使用了一個名稱為text
的props(屬性),在JSX中使用它就可以獲取得到設定的值。
當元件中的結構很複雜時,props(屬性)的值有很多不同種類時,對於屬性值不加上限制會變得麻煩,我們需要一個檢查的機制可以來預先聲明,哪一些props(屬性)是必要的或可選的,或是它們只能使用哪種資料類型,這對於打造可重覆使用的元件很重要。
React中可以使用類別中的propTypes
屬性來定義props(屬性)的限制,在執行時會對這些受限制的props(屬性)作檢查,propTypes
屬性需要定義為靜態屬性,如果是像TextShow
是一個使用 無狀態的功能性元件(Stateless functional components)的定義法,它的propTypes
屬性只能使用下面的語法再額外定義,這語句是寫在函式區塊的"外面":
TextShow.propTypes = {
text: React.PropTypes.string
}
這將會限制這個屬性只能使用字串類型,當你傳入其他資料類型時(例如數字),就會出現以下的警告訊息(不會中斷程式):
Warning: Failed prop type: Invalid prop `text` of type `number` supplied to `TextShow`, expected `string`.
不過這時候只會對text
作資料類型的檢查,如果你想要設定這個props(屬性)是必要的,需要再加上isRequired
在檢查類型的後面(連鎖語法),像下面這樣:
TextShow.propTypes = {
text: React.PropTypes.string.isRequired
}
當你沒使用這個props(屬性)時,就會出現以下的警告訊息(不會中斷程式):
Warning: Failed prop type: Required prop `text` was not specified in `TextShow`.
而另外一個TextInput
元件,它是使用ES6類別定義的,有兩種方式可以使用來定義propTypes
屬性。
第一種和上面類似,在類別定義區塊的"外面",加上下面的語法,程式碼如下:
TextInput.propTypes = {
initText: React.PropTypes.string.isRequired
}
第二種是ES7的語法,稱為"property initializers"(屬性初始子)。這需要在類別定義區塊的"裡面",加上static關鍵字,程式碼如下:
static propTypes = {
initText: React.PropTypes.string.isRequired
}
註: 這個ES7語法需要額外安裝babel工具中的"babel-preset-stage-2"或"babel-plugin-transform-class-properties",才能夠轉換程式碼。我們使用的create-react-app中已經有幫你裝好了。
關於其他的可使用的props驗証,請參考React官網這裡的說明,可用的規則還滿多的。不過你大概已經發現了,propTypes
只會出警告,並不會發生錯誤來中斷你的程式,也就是說它並不是一種強制性的類型限制。我們之後在其他的教學範例中會看到使用這些規則。
在程式碼的最上面,你可能已經有注意到加入了//@flow
。因為使用了Flow工具協助檢查,Flow對React的程式碼可以作一些特別的檢查,在這一節提供一些說明。
對於像TextShow
這樣的函式元件語法來說,它會針對函式的傳入參數(也就是props)作檢查,所以你需要加上props的物件結構,作為傳入參數的靜態類型,像下面這樣的程式碼:
const TextShow = (props: { text: string }) => (
<h1>{props.text}</h1>
)
props是一個物件的類型,裡面的結構就只能是{ text: string }
,代表它裡面只有一個text
屬性,而且值限定為字串類型。
當然,之前我也有提過在這種函式元件語法中,可以在傳入參數的地方使用解構的語法,如果像上面範例,在傳入參數用了解構賦值的話,會像下面這樣的語法:
const TextShow = ({ text = '' }: { text: string }) => (
<h1>{text}</h1>
)
另外,在TextInput元件中則展示了Flow工具,對於像類別元件的語法,如何加上標記來作state與props的語法,這兩種有些差異。
porps(屬性)的類型需要寫在整個類別之外,用type來定義一個類型別名,像下面這樣的語法:
type Props = {
initText: string,
}
然後在建構式中有用到props時,加上標記,像下面這樣的程式碼:
constructor(props: Props) {
super(props)
this.state = {
text: '',
}
}
state(狀態)的類型則是要寫在類別裡面,直接用state作類型設定就可以了,這在建構式的上面有這一行,就是針對state的靜態類型的結構設定:
state: {
text: string,
}
Flow工具可以協助我們,進行state與props的靜態類型的預先設定,相較於目前React只能用PropTypes
設定props
值類型的設計還要更具彈性,但目前這個檢查仍然只是多了一層保障而已,它與程式真正在執行時無關。對於Flow工具如何搭配React來使用,可以參考Flow官網的這篇文章。
props(屬性)也可以設定一個預設值,可以使用defaultProps
來給定某個props(屬性)的預設值,這個屬性的設定方式與上面的propTypes
屬性一模一樣。
所以在TextShow
元件中設定其中的text屬性的預設值語法,注意這是設定在函式的外面:
TextShow.defaultProps = {
text: '文字秀!'
}
在TextInput
元件中的定義getDefaultProps
的方式一樣有兩種。第一種是用在類別定義區塊外面的寫法:
TextInput.defaultProps = {
initText: '來輸入一些文字吧!'
}
第二種是ES7的語法:
static defaultProps = {
initText: '來輸入一些文字吧!'
}
這個預設值只會在元件的這個屬性,沒有被設定任何值時才會使用到。當有設定預設值(defaultProps
)時,propTypes
中的isRequired
(必要)檢查就會算通過了。
props
(屬性)中有關"擁有者-被擁有者(owner-ownee)" 的關係,這關係只會存在於React元件定義中,擁有者通常指的是某個位於上層的元件。規則很簡單:
render()
中建立了B元件,A就是B的擁有者。(或者可以說因為A元件的定義中設定了B元件的屬性)props
(屬性),這是它的擁有者才可以作的事。擁有者-被擁有者 關係對比DOM元素中的樹狀結構中的父母-子女關係,父母-子女是類似DOM中的直接的上下層結構關係,而擁有者-被擁有者 關係是定義時的設定props(屬性)的關係。以我們的例子來說:
"擁有者-被擁有者(owner-ownee)"的關係之所以會重要,因為它涉及到資料流(Data Flow)的概念。元件本身無法改變自己的props(屬性),對元件本身來說,自己的屬性是無法改變的(immutable),但它的擁有者元件可以。
這可以形成我們看到的第一種元件互相溝通的關係,由"擁有者-被擁有者(owner-ownee)"所建立的 - 擁有者 -> 被擁有者
的資料流,以我們的範例來說,也就是直接在TextInput(擁有者)元件中的定義裡,直接設定TextShow(被擁有者)元件的props屬性。
Components are Just State Machines (元件就是狀態的機器) ~ React官方文件
state(狀態)是元件中重要的特性,React認為元件就是狀態的機器,代表state(狀態)特性在元件中的重要程度。不過並不是所有的元件都要變成狀態機器,在你的應用程式中使用的組成元件們,大部份都是無狀態(Stateless)的元件,只有少數幾個會是有狀態的(Stateful)的元件。無狀態(Stateless)的元件一般都只會寫成函式元件的語法。
在React中的設計是,你只要簡單地更新元件的狀態(state),React會負責用最有效率的的方式來更新真實的DOM的樣子。對開發者來說,就是整個重新渲染的概念,一有小更動就整個全部渲染,有點類似不斷在瀏覽器中重新整理你的網頁。但在React中只是對Virtual DOM(虛擬DOM)作這件事而已,React自己會想辦法真正渲染到真實網頁上的DOM中。你可以把React想成是一個女僕或男管家,大小事都叫他去幫你處理就行了。當然為了在有些情況下,一定需要在真實的DOM元素上處理,React也提供了一些方式可以存取到真實的DOM元素。
那麼在React中,要怎麼觸發重新渲染的事件?
就是靠改變state(狀態)
,也就是使用setState
方法來改變原有的狀態值,就會觸發重新渲染(re-render)的事件,就這麼簡單而已。再次強調,state(狀態)
也只能靠setState
方法才能改變,這是一個強硬規則。
state(狀態)是代表會在程式執行時,會被改變的一種值,像我們的範例中,那個文字輸入框中的輸入文字,就會一直被改變(因為一直在輸入,它也只有這個功能…),很明顯的它就是一個state(狀態)
中的值。
state(狀態)一開始會在元件定義,因為state(狀態)也是個物件,所以會先定義它的初始值,以我們的範例來說是在建構式中定義this.state
物件,其中有一個text
屬性,初始值是空白字串:
//建構式
constructor() {
//super是呼叫上層父類別的建構式
super()
//設定初始的狀態。注意!這裡有個反樣式,不要用props的值來設定state的初始值。
this.state = {
text: ''
}
}
當使用者在文字框中不斷輸入字串時,要設計讓它會觸發state(狀態)
的改變,以此來觸發重新渲染,所以程式碼會像下面這樣寫,e.target.value
代表要把text
屬性的值改變為文字框當下輸入的值:
//處理的方法,用e.target可以獲取到輸入框的值,用箭頭函式可以綁定`this`
handleChange = (e) => {
this.setState({text: e.target.value})
}
因為state(狀態)
時,我們還需要把它顯示到TextShow
元件中,所以用上面的 擁有者-被擁有者 關係的資料傳遞方式,把TextShow元件標記中的text
屬性值先定義為{this.state.text}
,因為TextShow
元件位於render方法中,每次只要重新渲染時,text
屬性值也會跟著state(狀態)
的值改變。像下面的程式碼這樣:
//渲染方法,回傳React Element(元素)
render() {
return <div>
<TextShow text={this.state.text}/>
</div>
}
範例程式中整體的state(狀態)
使用,以及搭配被擁有者元件的props(屬性)
資料流,程式碼的流程就是這樣。
state(狀態)
的運用與概念並不困難,只要花點時間就能掌握。因為這個範例還算非常簡單的應用程式,之後的教學中的使用開始會複雜些。state(狀態)
是被鎖住在每個元件中使用的,這也造成state(狀態)
有一些限制,在複雜的使用者介面應用程式中,state(狀態)
需要搭配其他的架構來協助資料流的處理。
那props(屬性)相較於state(狀態)是什麼?
前面說了那麼多,props(屬性)就是元件的屬性值,在元件的標記裡設定,它的主要功能是設定元件中的"屬性"值用的,就像一顆西瓜中的屬性有重量、大小、產地、顏色、甜度之類的屬性。要特別注意,並不是說props(屬性)的值是不能改變的,而是元件不能改變自己本身的屬性,只有它的擁有者元件可以作這件事。
state(狀態)並不是props(屬性)的相反特性,這是一個常見的誤解,它們兩個是相關配合使用的屬性,只是某些其中的屬性設計成很類似。如果你有看到網路上的比較表,我建議你可以跳過,這兩個並不是相對的東西,拿來作比較反而會造成誤解。它們是相輔相成的特性。一個元件自主在內容中管理自己本身的state(狀態),它可以說是元件中"私有的(private)"特性。
props(屬性)可以先設定預設值,只是在沒有給定某個props(屬性)時自動給定這個預設值。state(狀態)代表的是在應用程式執行中一直改變的值,state(狀態)給定初始值只會在一開始第一次應用程式建立執行時,才使用這個初始值,之後應用程式就不會再使用它。state(狀態)的初始值的概念與props(屬性)的預設值不太一樣。
所以上面的程式碼註解中有一個反樣式,這個反樣式就是不要用props(屬性)來設定state(狀態)的初始值,根據官方(上一版的)的文章內容,單純的使用屬性值來設定作內部的狀態初始而已是可接受的樣式,但是如果還要利用props(屬性)的運算結果值就不能接受。這個原因是state(狀態)的初始值只會在應用程式第一次建立時使用,之後的重新渲染並不會再使用初始值。
在TextInput元件中,這個handleChange
方法,除了定義傳入參數的事件e是個Event物件外,另外在裡面也對e.target需要作檢查,它必須要是HTMLInputElement
的物件實體,才能夠獲取到value
值,這些都是因為使用了Flow工具,如果你不加這些檢查或標記,Flow工具會認為這個地方是有錯誤的。
handleChange = (e: Event) => {
// Flow會檢查必定要HTMLInputElement的物件才能有輸入值
if (e.target instanceof HTMLInputElement) {
this.setState({text: e.target.value})
}
}
當然,如果沒用Flow工具,程式碼寫起來很簡單,快要可以寫成一行了:
handleChange = (e) => {
this.setState({text: e.target.value})
}
Flow工具自然有它的好處,它可以針對是不是文字輸入框作一些預先的檢查,在DOM元素上的事件基本上原本就需要經過判斷,才能知道它是由文字輸入框輸入的,還是一般的DOM元素像div的觸發事件。我覺得這是要逐漸習慣的部份。
本章簡單的寫了一個範例,這是文字輸入欄位,並且用了state(狀態),以及props裡的propTypes
與defaultProps
設定。從這個小小的範例當中,我們可以學到以下重要的幾個知識: