深入 JSX

基本上,JSX 單純只是 React.createElement(component, props, ...children) function 的一個語法糖。以下 JSX 程式碼:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

會編譯成:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

如果沒有 children 的話你也可以使用閉合的標籤形式。例如:

<div className="sidebar" />

會編譯成:

React.createElement(
  'div',
  {className: 'sidebar'},
  null
)

如果你想測試某些特定的 JSX 會轉換成什麼樣的 JavaScript,你可以在線上 Babel 編譯器進行測試。

指定 React Element 類型

JSX 標籤的第一個部分決定 React element 的類型。

大寫字母的 JSX 標籤代表它們是 React Component。這些標籤會編譯成指向命名變數的 reference,所以當你使用 JSX <Foo /> 表達式時,Foo 就必須在作用域內。

React 必須在作用域內

因為 JSX 會編譯成呼叫 React.createElement 的形式,React 函式庫必須同時與你的 JSX 程式碼在相同的作用域內。

舉例來說,雖然 ReactCustomButton 並沒有在 JavaScript 中被直接使用,但是在此程式碼中它們必須被導入:

import React from 'react';
import CustomButton from './CustomButton';

function WarningButton() {
  // return React.createElement(CustomButton, {color: 'red'}, null);
  return <CustomButton color="red" />;
}

如果你不使用任何 JavaScript bundler 並從 <script> 標籤來載入 React,那麼它就已經以 React 存在於作用域中了。

在 JSX 類型中使用點記法

你也可以在 JSX 中使用點記法來指向一個 React Component。這對當你有一個會導出許多 React component 的 module 來講是十分方便的。舉例來說,如果 MyComponents.DatePicker 是一個 component,那麼你可以在 JSX 中直接這樣使用:

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

使用者定義的 Component 必須由大寫字母開頭

當一個 element 為小寫字母開頭時,它就會指向內建的 component,例如 <div><span> 會生產成字串 'div''span' 並傳遞給 React.createElement。像 <Foo /> 大寫字母開頭的 element 會編譯成 React.createElement(Foo) 並且在你的 JavaScript 檔案裡對應自定義或導入的 component。

我們建議以大寫字母開頭來命名 component。如果你有一個小寫字母開頭的 component,請在 JSX 裡使用之前把它賦值給一個大寫字母開頭的變數。

例如,以下程式碼並不會按照預期運行:

import React from 'react';

// 錯誤!這是一個 component 並且應該由大寫字母開頭:
function hello(props) {
  // 正確!因為 div 是一個有效的 HTML 標籤,所以使用<div> 是可行的:
  return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
  // 錯誤!React 會因為非大寫字母開頭而認為 <hello /> 是一個 HTML 標籤:
  return <hello toWhat="World" />;
}

為了解決這個問題,我們將重新命名 helloHello 並且以 <Hello /> 來使用它:

import React from 'react';

// 正確!這是一個 component 並且應該由大寫字母開頭:
function Hello(props) {
  // 正確!因為 div 是一個有效的 HTML 標籤,所以使用 <div> 是可行的:
  return <div>Hello {props.toWhat}</div>;
}

function HelloWorld() {
  // 正確!React 會因為大寫字母開頭而了解 <Hello /> 是一個 component。
  return <Hello toWhat="World" />;
}

在 Runtime 時選擇類型

你不能用通用表達式當作 React element 的類型。如果你想要用通用表達式來表示 element 的類型,你可以先把它賦值給一個大寫字母開頭的變數。這是在當你想要根據一個 prop 來決定 render 不同 component 時常常發生的:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 錯誤!JSX 不能是表達式。
  return <components[props.storyType] story={props.story} />;
}

為了解決這個問題,我們首先將類型賦值給一個大寫字母開頭的變數:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正確!JSX 類型可以是大寫字母開頭的變數。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

JSX 中的 Props

在 JSX 中有不同的方式可以指定 props。

JavaScript 表達式作為 Props

你可以用 {} 包住任何 JavaScript 表達式作為一個 prop 傳遞。例如,在以下 JSX 中:

<MyComponent foo={1 + 2 + 3 + 4} />

MyComponent 來說,因為 1 + 2 + 3 + 4 會被解讀,所以 props.foo 的值會是 10

因為 if 語法與 for 迴圈都不屬於 JavaScript 表達式,所以它們並不能在 JSX 中被直接使用。不過,你可以在它周圍外的程式碼中使用。例如:

function NumberDescriber(props) {
  let description;
  if (props.number % 2 == 0) {
    description = <strong>even</strong>;
  } else {
    description = <i>odd</i>;
  }
  return <div>{props.number} is an {description} number</div>;
}

你可以在對應的段落中了解更多關於條件式 render迴圈

字串字面值

你可以傳遞一個字串字面值作為 prop。以下兩個 JSX 表達式是相等的:

<MyComponent message="hello world" />

<MyComponent message={'hello world'} />

當你傳遞一個字串字面值時,它的值是未經 HTML 轉義的。所以以下兩個 JSX 表達式是相等的:

<MyComponent message="&lt;3" />

<MyComponent message={'<3'} />

這種行為通常是無關緊要的。在此只是為了完整性而提及。

Props 預設為 「True」

如果你沒給 prop 賦值,那麼它的預設值就是 true。以下兩個 JSX 表達式是相等的:

<MyTextBox autocomplete />

<MyTextBox autocomplete={true} />

一般來說,我們因為容易把它跟 ES6 object shorthand 混淆,{foo}{foo: foo} 的簡寫而不是 {foo: true},所以並不建議這樣使用。這種行為存在只是為了相配 HTML 的行為。

展開屬性

如果你已經有了一個 props 的 object,並且想把它傳遞進 JSX,你可以使用 ... 作為展開運算子來傳遞整個 props object。以下兩個 component 是相等的:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

你也可以使用展開運算子來分開並挑選 component 所需的 props。

const Button = props => {
  const { kind, ...other } = props;
  const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  return <button className={className} {...other} />;
};

const App = () => {
  return (
    <div>
      <Button kind="primary" onClick={() => console.log("clicked!")}>
        Hello World!
      </Button>
    </div>
  );
};

在以上的範例中,kind prop 被安全地挑出並且不會被傳遞進 DOM 中的 <button> element。 所有其它的 props 藉由 ...other object 被傳遞,讓 component 的應用非常具有彈性。你可以看見它傳遞一個 onClickchildren props。

展開運算子不但可以如此靈活地使用,它能讓我們輕易挑選出對於 component 不重要且多餘的 props,也能讓我們傳遞無效的 HTML 屬性到 DOM 裡。

JSX 中的 Children

在 JSX 表達式有包含開始與結束標籤的情形下,夾在兩者之間的內容會被傳遞為特別的 prop:props.children。有幾種不同的方法來傳遞 children:

字串字面值

你可以在兩個標籤之間放置字串,而 props.children 就會是那個字串。這對許多內建的 HTML element 是很有用的。例如:

<MyComponent>Hello world!</MyComponent>

這是有效的 JSX,而 props.childrenMyComponent 中單純就會是 "Hello world!"。HTML 是未經轉義的,所以你可以這樣如同 HTML 來寫 JSX:

<div>This is valid HTML &amp; JSX at the same time.</div>

JSX 會把開頭與結尾的空白去除,也會去除空行。與標籤相鄰的新行會去除;在字串字面值中間的新行則會被壓縮成單一空格。所以以下都會 render 同樣的結果:

<div>Hello World</div>

<div>
  Hello World
</div>

<div>
  Hello
  World
</div>

<div>

  Hello World
</div>

JSX Children

你可以提供許多 JSX element 作為 children。這在顯示巢狀 component 時是非常實用的:

<MyContainer>
  <MyFirstComponent />
  <MySecondComponent />
</MyContainer>

你可以混合不同類型的 children,所以你也能夠同時使用字串字面值與 JSX children。這也是 JSX 與 HTML 另一相似的點,而以下是有效的 JSX 同時也是有效的 HTML:

<div>
  Here is a list:
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
</div>

一個 React component 也能夠回傳一個陣列 element:

render() {
  // 沒有必要把多餘的 list items 包在 element 裡頭!
  return [
    // 別忘了加 keys :)
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}

JavaScript 表達式作為 Children

你可以傳遞任何封裝在 {} 內的 JavaScript 表達式作為 children。例如,以下表達式皆相等:

<MyComponent>foo</MyComponent>

<MyComponent>{'foo'}</MyComponent>

這在要 render 任意長度的 JSX 表達式列表時是非常實用的。例如,這會 render 一個 HTML 列表:

function Item(props) {
  return <li>{props.message}</li>;
}

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}

JavaScript 表達式可以與不同類型的 children 混合。這在不使用樣板字串時非常實用:

function Hello(props) {
  return <div>Hello {props.addressee}!</div>;
}

Functions 作為 Children

正常來說,在 JSX 中的 JavaScript 表達式會被轉換成字串、React element、或者包含這些的列表。不過,props.children 就像其它 prop 一樣可以傳遞任何類型的資料,而並不局限於只有 React 知道如何 render 的資料。舉例來說,假如你有一個自訂 component,你可以把 callback 作為 props.children 傳遞:

// numTimes 次呼叫 children callback 來重複生成 component
function Repeat(props) {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

function ListOfTenThings() {
  return (
    <Repeat numTimes={10}>
      {(index) => <div key={index}>This is item {index} in the list</div>}
    </Repeat>
  );
}

被傳遞進自訂 component 的 children 可以是任何東西,只要 component 能夠在 render 之前把它轉換成 React 能夠理解的東西就可以了。這種用法並不普遍,但能夠顯示出 JSX 的延伸性。

Booleans, Null, 與 Undefined 會被忽略

false, null, undefined, 與 true 都是有效的 children。它們只是單純不會被 render。以下 JSX 表達式皆會 render 相同的結果:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

這在需要條件式 render 不同 React elements 時非常方便。以下 JSX 只會在 showHeadertrue 時 render <Header />

<div>
  {showHeader && <Header />}
  <Content />
</div>

值得注意的是有一些像是數字 0「falsy」值 仍然會被 React 給 render。舉例來說,以下的程式碼可能不會如同你預期般地運作,因為當 props.messages 是一個空 array 時, 0 會被印出:

<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>

為了解決這個問題,確保在 && 之前的表達式為 boolean:

<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

相反地,如果你想要印出 falsetruenull 或者 undefined 時,你必須要先把它轉換成一個字串

<div>
  My JavaScript variable is {String(myVariable)}.
</div>