reactjs 語法

JSX

1
const element = <h1>hello</h1>;

這個語法叫做 JSX,是一個 JavaScript 的語法擴充,會產生 React element

Babel 將 JSX 編譯為呼叫 React.createElement() 的程式。

React DOM 預設會在 render 之前 escape 所有嵌入在 JSX 中的變數。這保證你永遠不會不小心注入任何不是直接寫在你的應用程式中的東西。所有變數都會在 render 之前轉為字串,這可以避免 XSS(跨網站指令碼)攻擊。

大括號內代表變數,可以放入任何javascript expression

1
2
3
4
5
6
7
8
9
10
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

//多行:包在小括號中
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);

Render

使用 React 建立應用程式時,通常會有一個單一的 root DOM node。

Render 一個 React element 到 root DOM node

1
2
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

React element 是 immutable 的,即不能更改

Element 就像是電影中的一個幀:它代表特定時間點的 UI。

Component

Component 就像是 JavaScript 的 function,它接收參數(稱之為「props」,屬性)並且回傳描述畫面的 React element。

Component 的字首須為大寫字母。React 會將小寫字母開頭的組件視為原始 DOM 標籤,舉例來說,<div /> 就會被視為是 HTML 的 div 標籤,但是 <Welcome /> 則是一個 component

props 是唯讀的

1
2
3
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}

此 function 是一個符合規範的 React component,因為它接受一個 props 物件並回傳一個 React element。

我們稱之為 function component

也可以使用 ES6 Class 來定義 component:

1
2
3
4
5
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

可以在JSX中使用component

1
const element = <Welcome name="Sara" />;

通常來說,每個 React 應用程式都有一個最高層級的 App component。然而,如果你將 React 結合至現存的應用程式中,你可能需要使用像 Button 這樣的小型 component,並由下往上,逐步應用到畫面的最高層級。

在較大的應用程式中,建構可複用的 component 是非常值得的

State

state 類似於 prop,但它是私有且由 component 完全控制的。

需使用 class component,不能在 function component 使用

在每次發生更新時,render 方法都會被呼叫

1
2
3
4
5
6
7
8
9
10
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

加入state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}

render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}

每當 Clock render 到 DOM 的時候,在 React 中稱為「mount」。

每當產生的 Clock DOM 被移除時,在 React 中稱為「unmount」。

在class內加入 componentDidMount(), componentWillUnmount(), 和 setState(),用 state 實作每秒更新的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}

componentWillUnmount() {
clearInterval(this.timerID);
}

tick() {
this.setState({
date: new Date()
});
}

因為 props 和 state 可能是非同步的被更新,你不應該依賴它們的值來計算新的 state。

要修正這個問題,使用第二種形式的 setState(),它接受一個 function 而不是一個 object。Function 將接收先前的 state 作為第一個參數,並且將更新的 props 作為第二個參數:

1
2
3
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

React是「上至下」或「單向」的資料流:任何 state 總是由某個特地的 component 所擁有,任何從 state 得到的資料或 UI,state 只能影響自身和 child component。

事件處理

事件的名稱在 React 中都是 camelCase,而在 HTML DOM 中則是小寫。

  • HTML DOM 對照 React
    • onclickonClick
    • onclick="click()"onClick={click}

在 React 中,你不能夠在像在 HTML DOM 中使用 return false 來避免瀏覽器預設行為。你必須明確地呼叫 preventDefault

1
2
3
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>

在 React 中,你則可以這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}

return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}

在 JavaScript 中,class 的方法在預設上是沒有被綁定(bound)的。如果你忘了綁定 this.handleClick 並把它傳遞給 onClick 的話,this 的值將會在該 function 被呼叫時變成 undefined。

總之,當你使用一個方法,卻沒有在後面加上 () 之時(例如當你使用 onClick={this.handleClick} 時),你應該要綁定這個方法。

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};

// 為了讓 `this` 能在 callback 中被使用,這裡的綁定是必要的:
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}

ReactDOM.render(
<Toggle />,
document.getElementById('root')
);

如果呼叫 bind 對你來說很麻煩的話,你可以用別的方式。如果你使用了還在測試中的 class fields 語法的話,你可以用 class field 正確的綁定 callback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoggingButton extends React.Component {
// 這個語法確保 `this` 是在 handleClick 中被綁定:
// 警告:這是一個還在*測試中*的語法:
handleClick = () => {
console.log('this is:', this);
}

render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}

傳遞一個額外的參數給 event handler。例如,如果 id 是每一行的 ID 的話,下面兩種語法都可行:

1
2
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

以上這兩行程式是相同的。一個使用 arrow functions,另一個則使用了 Function.prototype.bind。

以這兩個例子來說,e 這個參數所代表的 React 事件將會被當作 ID 之後的第二個參數被傳遞下去。在使用 arrow function 時,我們必須明確地將它傳遞下去,但若使用 bind 語法,未來任何的參數都將會自動被傳遞下去。

條件render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}

return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}

&& 來條件顯示
因為在 JavaScript 中,true && expression 總是回傳 expression ,而 false && expression 總是回傳 false

1
2
3
4
5
6
7
8
9
10
11
12
13
  function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}

React 在遇到 nullfalse 時都會忽略(不顯示)

或用條件運算式

1
2
3
4
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}

列表與key

1
2
3
4
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
<li>{number}</li>
);

你會收到一個關於你應該提供 key 給每一個列表項目的警告。key 是當你在建立一個 element 列表時必須使用的特殊的 string attribute。

key 幫助 React 分辨哪些項目被改變、增加或刪除。在 array 裡面的每個 element 都應該要有一個 key,如此才能給予每個 element 一個固定的身份:

通常,你會使用資料的 ID 作為 key:

1
2
3
4
5
6
7
8
9
10
11
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);

const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);

當你 render 的項目沒有固定的 ID 且你也沒有更好的辦法時,你可以使用項目的索引做為 key:

1
2
3
4
5
6
const todoItems = todos.map((todo, index) =>
// 請在項目沒有固定的 ID 時才這樣做
<li key={index}>
{todo.text}
</li>
);

React 預設將會使用索引作為 key。但並不建議你使用索引作為 key,尤其如果項目的順序會改變的話。這會對效能產生不好的影響,也可能會讓 component state 產生問題

  • key 必須在 Sibling 中是唯一的
  • key 的功能是提示 React,但它們不會被傳遞到 component 的 prop。

應該把 key 放在 array 產生 item 的 render,而不是把它放在 item 的 render。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function ListItem(props) {
const value = props.value;
return (
// 錯!你不需要在這裡指出 key:
<li key={value.toString()}>
{value}
</li>
);
}

function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 錯!你應該要在這裡指出 key:
<ListItem value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}

一個好的經驗法則是,在 map() 呼叫中的每個 element 都會需要 key。

Form

把這個表單寫成一個 controlled component: 用 state 來 顯示 value,onChange 時更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};

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

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

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

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

這意味著你必須寫更多的 code,但現在你同時可以將 value 傳遞給其他的 UI element,或是從其他 event handler 重置。

Select

在 React 中並不是用 selected attribute,而是在 select 的標籤上用一個 value attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};

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

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

handleSubmit(event) {
alert('Your favorite flavor is: ' + this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite flavor:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
}
}

你可以將一個 array 傳給 value 這個 attribute,這使得你可以在一個 select 中選取多個選項:

1
<select multiple={true} value={['B', 'C']}>

input file

1
<input type="file" />

由於它的值是唯讀,它在 React 中是一個 uncontrolled component。在稍後的文件中有其他關於它和其他 uncontrolled component 的討論。

多個input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};

this.handleInputChange = this.handleInputChange.bind(this);
}

handleInputChange(event) {
const target = event.target;
const value = target.name === 'isGoing' ? target.checked : target.value;
const name = target.name;

this.setState({
[name]: value
});
}

render() {
return (
<form>
<label>
Is going:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}

在一個 controlled component 上指明 value prop 可避免使用者改變輸入,除非你希望使用者這樣做。如果你已經指明了 value 但輸入仍然是可以被修改的,你很可能是不小心將 value 的值設定為 undefined 或 null。

下面的程式碼就是一個範例。(輸入原先是被鎖住的,但在短暫的延遲後,變得可以被修改了。)

1
2
3
4
5
ReactDOM.render(<input value="hi" />, mountNode);

setTimeout(function() {
ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

uncontrolled component

在大多數的情況下,我們推薦使用 controlled component 來實作表單。在控制元件裡,表單的資料是被 React component 所處理。另一個選擇是 uncontrolled component,表單的資料是由 DOM 本身所處理的。

使用 ref 來從 DOM 取得表單的資料,而不是為了每個 state 的更新寫 event handler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NameForm extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.input = React.createRef();
}

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

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

如果你想找出一個完整的、包含驗證、可追蹤拜訪欄位並能處理提交表單等功能的解決方案,Formik 是一個很熱門的選擇。然而,它是在與 controlled component 和維持 state 相同的原則上所建立的,所以別忘了學習它。

提升 State

在 React 中,將 state 搬移到需要它的 component 的共同最近的祖先來共享 state。這被稱為「提升 state」。

我們將從 TemperatureInput 移除 local state 並且搬移它到 Calculator。

我們移除了它的 local state,並且不讀取 this.state.temperature,我們現在讀取 this.props.temperature。當我們想要改變時不呼叫 this.setState(),我們現在呼叫 this.props.onTemperatureChange(),它是由 Calculator 提供的

calculator 傳入 prop.onChange 方法,供child的值改變時呼叫onChange(),以改變calculator(parent)的值

通常來說,state 會優先被加入到需要 render 的 component。接著,如果其他的 component 也需要的話,你可以提升 state 到共同最靠近的 ancestor。

你應該依賴上至下的資料流,而不是嘗試在不同 component 之間同步 state。

當你在 UI 上看到一些錯誤時,你可以使用 React Developer Tools 來檢查 prop 並往 tree 的上方尋找,直到找到負責更新 state 的 component。這讓你可以追蹤到錯誤的來源

合成 vs 繼承

有些 component 不會提早知道它們的 children 有些什麼。對於像是 Sidebar 或 Dialog

我們建議這些 component 使用特殊的 children prop 將 children element 直接傳入到它們的輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}

//任何在 <FancyBorder> JSX tag 內的內容都被作為 children prop 被傳遞給 FancyBorder component。
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}

可用來排版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}

function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}

在 Facebook 中,我們使用 React 在成千上萬個 component,我們找不到任何使用案例來推薦你建立繼承結構的 component。

用 React 思考

第一步:將 UI 拆解成 component 層級

首先,你要做的是將視覺稿中每一個 component (及 subcomponent)都圈起來,並幫它們命名。

其中一個技巧是單一職責原則,它的意思是:在我們的理想中,一個 component應該只負責做一件事情。如果這個 component 最後變大了,你就需要再將它分成數個更小的 subcomponent 。

由於你常常會展示 JSON 的資料模型給使用者,你會發現,如果你的模式是正確地被建立的話,你的 UI(以及你的 component 結構)會很好的相互對應。這是因為 UI 和資料模型通常是遵守同樣的資訊架構,這意味著將你的 UI 拆成 component 通常是相當容易的。

第二步:在 React 中建立一個靜態版本

在你有了 component 層級後,就可以開始實作你的應用程式了。最簡單的方式是為你的應用程式建立一個接收資料模型、render UI 且沒有互動性的版本。建立一個靜態版本需要打很多字,但不需要想很多,而加上互動性則相反,需要做很多的思考,很少的打字,所以最好的方式是把這幾個過程都分開來。接下來,我們會知道為什麼是如此。

請完全不要使用 state 來建立這個靜態版本。State 是保留給互動性的,也就是會隨時間改變的資料。既然我們目前要做的是這應用程式的靜態版本,你就不需要 state。

你可以從最上層開始,或從最下層開始。也就是說,你可以先從層級較高的 component 開始做起,或者你也可以從比它低層級的開始。在比較簡單的例子中,通常從上往下是比較簡單的。但在較為大型的專案中,從下往上、邊寫邊測試則比較容易。

React 的 單向資料流(也可稱為單向綁定)確保所有 component 都是模塊化且快速的。

第三步:找出最少(但完整)的 UI State 的代表

請找出你的應用程式所需的最少的呈現方式,並在你遇到其他東西時再計算它們。例如,如果你在建立一個待辦清單,使用一個可以用來代表待辦事項的 array。不要另外用一個獨立的 state 變數來追蹤數量。當你要 render 代辦事項的數量時,讀取待辦事項 array 的長度即可。

對於每一個資料,問你自己這三個問題:

  1. 這個資料是從 parent 透過 props 傳下來的嗎?如果是的話,那它很可能不是 state。
  2. 這個資料是否一直保持不變呢?如果是的話,那它很可能不是 state。
  3. 你是否可以根據你的 component 中其他的 state 或 prop 來計算這個資料呢?如果是的話,那它一定不是 state。

第四步:找出你的 State 應該在哪裡

我們需要找出哪幾個 component 會 mutate,或者擁有,這個 state。

請記得,React 的核心精神是單向資料流,從 component 的層級從高往下流。也許哪個 component 該擁有 state 在一開始並不是很明顯。對新手來說,這往往是最難理解的概念,所以請跟著以下的步驟來思考:

在你的應用程式中的每個 state:

  • 指出每個根據 state 來 render 某些東西的 component。
  • 找出一個共同擁有者 component(在層級中單一一個需要 state 的、在所有的 component 之上的 component)。
  • 應該擁有 state 的會是共同擁有者 component 或另一個更高層級的 component。(找最高級的component來持有state)
  • 如果你找不出一個應該擁有 state 的 component 的話,那就建立一個新的 component 來保持 state,並把它加到層級中共同擁有者 component 之上的某處。

第五步:加入相反的資料流

現在是時候支援另一種資料流的方向了:在層級深處的表格 component 需要更新 FilterableProductTable 的 state。

讓我們思考一下我們想要做些什麼。我們想確保當使用者改變這個表格時,我們會更新 state 以反映使用者的輸入。既然 component 只應該更新它自己本身的 state, FilterableProductTable 將會把 callback 傳給 SearchBar,而它們則會在 state 該被更新的時候被觸發。我們可以在輸入上使用 onChange 這個 event 來 接收通知。被 FilterableProductTable 傳下來的 callback 則會呼叫 setState(),之後應用程式就會被更新。

推薦的 Toolchain

React 團隊主要推薦以下的方案:

  • 如果你正在學習 React 或建立全新的 single-page 應用程式,請使用 Create React App。
  • 如果你正在建立一個使用 Node.js 的 server-rendered 網頁,請使用 Next.js。
  • 如果你正在建立一個靜態內容的網頁,請使用 Gatsby。
  • 如果你正在建立一個 component 函式庫或與現存程式碼倉庫進行接軌,請使用更靈活的 Toolchain。

Reference

  • React: 語法教學
  • React: 動手做教學
  • [React: css])(https://reactjs.org/docs/faq-styling.html)