終究都要學 React 何不現在學呢? - React 進階 - 深入 JSX - (12)

前言

前面章節雖然我們已經認識了基本的 JSX 的部分,但這一篇我們會更深入的認識了解 JSX,讓我們在開發的時候可以少一點雷點,甚至活用一些技巧。

createElement

還記得我們最早前面是怎麼建立 React DOM 的嗎?

忘了也沒關係,我們一開始都是使用 React.createElement 來建立畫面

1
2
3
4
5
6
7
8
const app = document.querySelector('#root');
const root = ReactDOM.createRoot(app);

const element = React.createElement('h1', {
children: 'Hello React',
})

root.render(element);

不可否認的是使用 React.createElement 來建立 React DOM 真的很麻煩,而且也不太直覺,所以我們實際開發上還是以 JSX 為主。

JSX 就是語法糖

就如同前面所言使用 React.createElement 來去撰寫 DOM 實在太困難,因此 React 才會出一個語法糖,也就是 JSX,而本身 JSX 就是 React.createElement 的語法糖。

因此舉例來講以下語法

1
const example = <h1>Hello React</h1>;

會被編譯成

1
const example = /*#__PURE__*/React.createElement("h1", null, "Hello React");

因此 JSX 本質上就是一個 createElement 語法糖。

如果是剛剛前面的 TodoList 巢狀範例(有刪除多餘程式碼,以便辨識)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const List = () => {
return (
<ul>
<li>這是列表</li>
</ul>
)
}

const App = () => {
return (
<div>
<h1>Hello React</h1>
<List/>
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

則會編譯成以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const List = () => {
return /*#__PURE__*/(
React.createElement("ul", null, /*#__PURE__*/
React.createElement("li", null, "\u9019\u662F\u5217\u8868")));


};

const App = () => {
return /*#__PURE__*/(
React.createElement("div", null, /*#__PURE__*/
React.createElement("h1", null, "Hello React"), /*#__PURE__*/
React.createElement(List, null)));
};

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render( /*#__PURE__*/React.createElement(App, null));

透過前面範例我們可以知道一件事情,如果從頭到尾都用 createElement 撰寫的話,是真的會發瘋的。

自閉合 (self-closing)

舉例來講,下方有一個 HTML header 標籤,而不寫入任何內容

1
2
3
4
const App = () => <header className="text-center header"></header>

const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render(<App />);

是可以使用自閉合的方式關閉標籤,達到更簡潔的狀況

1
2
3
4
const App = () => <header className="text-center header" />

const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render(<App />);

而這兩者生出來的東西依然都是相同的

1
2
3
4
const App = () => /*#__PURE__*/React.createElement("header", { className: "text-center header" });

const root = ReactDOM.createRoot(document.querySelector("#root"));
root.render( /*#__PURE__*/React.createElement(App, null));

就算是元件也是一樣,以 TodoList 的章節就是這樣子撰寫

1
<List />

透過這方式,可以讓我們用更優雅簡潔的方式去撰寫 JSX。

點運算子命名空間

JSX 中有一個很有趣的做法,你可以宣告一個物件,裡面放許多元件甚至也可以傳入 Props

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
const obj = {
MyName({ name }) {
return (<h1>Hello { name }</h1>)
},
List() {
return (
<ul>
<li>列表1</li>
<li>列表2</li>
</ul>
)
}
}

const App = () => {
return (
<div>
<h1>Hello React</h1>
<obj.myName name="Ray"/>
<obj.List />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

自定義的 Components 首字必須為大寫

前面章節你可以發現我們在建立一個 React 元件時,都是採用首字大寫的形式

1
2
3
4
5
6
7
8
9
10
11
const Cart = () => {
// ... 略
}

const List = () => {
// ... 略
}

const App = () => {
// ... 略
}

但我們並沒有提到原因是為什麼,其實原因是因為當你在撰寫 React 元件時,如果你的元件名稱首字是小寫的話,那麼 React 會認為你是在撰寫一個原生的 HTML 標籤,而不是一個自定義的元件。

什麼意思呢?這邊先來看一段範例

1
2
3
4
5
6
7
8
9
10
11
12
const App = () => {
return (
<div>
<header>Header</header>
<div>Hello React</div>
<footer>Footer</footer>
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

透過上方範例,我們可以看到編譯後的結果如下

1
2
3
4
5
6
7
8
9
10
const App = () => {
return /*#__PURE__*/(
React.createElement("div", null, /*#__PURE__*/
React.createElement("header", null, "Header"), /*#__PURE__*/
React.createElement("div", null, "Hello React"), /*#__PURE__*/
React.createElement("footer", null, "Footer")));
};

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render( /*#__PURE__*/React.createElement(App, null));

你可以看到 React.createElement 的第一個參數都是一個單純的字串。

如果將上方範例程式碼改成以下呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const header = () => <header>Header</header>;
const div = () => <div>Hello React</div>;
const footer = () => <footer>footer</footer>;

const App = () => {
return (
<div>
<header />
<div />
<footer />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

這時候會發現一件有趣的事情,你會發現畫面完全不會出現任何東西,這是因為 React 認為你是在撰寫一個原生的 HTML 標籤,而不是撰寫元件,所以實際上 React 編譯出來的程式碼是這樣的

1
2
3
4
5
6
7
8
9
10
11
12
13
const header = () => /*#__PURE__*/React.createElement("header", null, "Header");
const div = () => /*#__PURE__*/React.createElement("div", null, "Hello React");
const footer = () => /*#__PURE__*/React.createElement("footer", null, "footer");

const App = () => {
return /*#__PURE__*/(
React.createElement("div", null, /*#__PURE__*/
React.createElement("header", null), /*#__PURE__*/
React.createElement("div", null), /*#__PURE__*/
React.createElement("footer", null)));
};
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render( /*#__PURE__*/React.createElement(App, null));

你會發現 React.createElement 的第一個參數都是一個單純的字串,而不是元件名稱。

接著讓我們看一下首字大寫的元件又會是如何

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Header = () => <header>Header</header>;
const Div = () => <div>Hello React</div>;
const Footer = () => <footer>footer</footer>;

const App = () => {
return (
<div>
<Header />
<Div />
<Footer />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

基本上你會看到畫面正常出現了,接著我們看一下編譯後的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.clear();
const Header = () => /*#__PURE__*/React.createElement("header", null, "Header");
const Div = () => /*#__PURE__*/React.createElement("div", null, "Hello React");
const Footer = () => /*#__PURE__*/React.createElement("footer", null, "footer");

const App = () => {
return /*#__PURE__*/(
React.createElement("div", null, /*#__PURE__*/
React.createElement(Header, null), /*#__PURE__*/
React.createElement(Div, null), /*#__PURE__*/
React.createElement(Footer, null)));
};

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render( /*#__PURE__*/React.createElement(App, null));

我們可以看到 React.createElement 的第一個參數不再是單純的字串,而是傳入一個變數,這也就是為什麼我們在撰寫元件時,首字母必須大寫的原因。

Props 預設為 True

前面我們有介紹到 Props 的概念,但是在 JSX 中,如果你沒有給予 Props 值,那麼預設值就會是 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const List = ({ data }) => {
return (
<ul>
<li>{ JSON.stringify(data) }</li>
</ul>
)
}

const App = () => {
return (
<div>
<List data />
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

而這個結果與你主動傳入 true 是一樣的

1
<List myName={ true }/>

只是以官方建議來講,還是會建議你要傳遞 Props 的資料,主要是避免與物件縮寫的觀念混淆,因為原本的物件寫法中,假設物件的 key 與 value 若相同,如:{ myName: myName } 是可以改寫成這樣的 { myName },而這是一個單純的物件縮寫。

空格去除

JSX 預設狀況下是會清除開頭與結尾的空格,就算是換行符號也是一樣這一點要多加注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const App = () => {
return (
<div>
<div>Hello React</div>

<div>
Hello React
</div>

<div>
Hello
React
</div>

<div>

Hello React
</div>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

上面範例我們可以發現,不管怎麼用都是保持著一行文字的方式呈現。

特定型別會被忽略

JSX 另一個有趣的地方在於 BooleanNullUndefined 是無法直接出現在畫面上的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const App = () => {
return (
<div>
<div>Boolean:{ true }</div>

<div>Boolean:{ false }</div>

<div>Null:{ null }</div>

<div>Undefined:{ undefined }</div>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

但這並不代表 JSX 會發生錯誤,而是單純不會被 render 而已。

如果想要被輸出的話,可以使用 String 來做型別轉換,只要轉換成一個字串就可以正常輸出了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const App = () => {
return (
<div>
<div>Boolean:{ String(true) }</div>

<div>Boolean:{ String(false) }</div>

<div>Null:{ String(null) }</div>

<div>Undefined:{ String(undefined) }</div>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

當然其實不只有使用 String 這種方式,以下我也列出了各種解決方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const App = () => {
return (
<div>
<div>Boolean:{ String(true) }</div>

<div>Boolean:{ true.toString() }</div>

<div>Boolean:{ true + '' }</div>

<div>Boolean:{ `${true}` }</div>

<div>Boolean:{ JSON.stringify(true) }</div>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

後記

本文將會同步更新到我的部落格