React.js
React基礎
JSX語法

在過去使用原生 JS 程式碼開發網頁時,常常遇到一個問題: 你會發現類似以下的程式碼不斷的在重複:

let menuContainer = document.createElement('div');
menuContainer.setAttribute('class', 'menu-container');
 
//藍色標題
let title = document.createElement('p');
title.setAttribute('class', 'menu-title');
title.textContent = 'Andy Chang的Like';
menuContainer.appendChild(title);

這樣的過程理論上也應該要被模組化包成函式比較好,於是 React 提供了一個類似的 API:

React.createElement('div', undefined, 'hello world');

除了第二個參數不知道為啥是undefined外,其他你應該都猜的出來是要做甚麼的。

實際上第一個參數是 component,第二個參數是 props,第三個參數是 children。現在不知道這些是啥沒關係,我們後面會提到

當然,React 不只是包了「創造一般元素的功能」進去這個 API 裡面,有興趣的可以去看看 React 的 source code。

然而這樣還是不夠直覺。

於是就產生了「讓 HTML 可以直接寫在 Javascript 的語法」 - JSX

JSX 的語法是什麼呢?

在 React 16 之前,Babel 遇到 JSX 語法的時候會自動幫我們轉成React.createElement。以下是 JSX 的一些規定:

1. HTML 語法可以當作參數、變數值傳遞

例如,下方是合法的語法,會跟前一篇的 Hello world 一樣:

import React from 'react';
import { createRoot } from 'react-dom/client';
 
const menuFactory = () => {
    return <div>hello world!</div>;
};
 
const root = createRoot(document.getElementById('root'));
root.render(menuFactory());

2. 傳遞 HTML 時,只能傳遞一個標籤元素

🚫

意思是下方這樣是錯誤的:

import React from 'react';
import { createRoot } from 'react-dom/client';
 
const root = createRoot(document.getElementById('root'));
root.render(<div>hello world!</div><button>我是按鍵</button>);

那如果我們想要傳遞多個元素怎麼辦?這個時候就要用一個 container 把元素包起來:

import React from 'react';
import ReactDOM from 'react-dom';
 
root.render(
    <div>
        <div>hello world!</div>
        <button>我是按鍵</button>
    </div>
);

但是這樣很容易多出一堆沒有用的 container。為了解決這個問題,React 提供了一個叫 Fragment 的元件。

import React, { Fragment } from 'react';
import { createRoot } from 'react-dom/client';
 
const root = createRoot(document.getElementById('root'));
root.render(
    <Fragment>
        <div>hello world!</div>
        <button>我是按鍵</button>
    </Fragment>
);

實際渲染到畫面上時,React 會自己把 Fragment 去除掉

另外如果嫌 Fragment 這個單字太長,React 也有提供Fragment 簡寫的語法:<></>

import React from 'react';
import { createRoot } from 'react-dom/client';
 
const root = createRoot(document.getElementById('root'));
root.render(
    <>
        <div>hello world!</div>
        <button>我是按鍵</button>
    </>
);

效果和剛剛一模一樣。

3. 可以在 HTML 標籤中利用「」寫 Javascript 表示式

以下面這個範例而言 :

import React from 'react';
import { createRoot } from 'react-dom/client';
 
const root = createRoot(document.getElementById('root'));
root.render(<div> {1 + 1} </div>);

執行結果是:

請注意如果要在{}中使用字串,就必須要使用"文字"或是'文字',因為{}中就等於是 Javascript 語法了。相反的像truefalse這種布林值就不能加上引號。

4. 「class」屬性變成「className」。

//這是正確的寫法
root.render(
    <ul className="menu">
        <li className="menu-item">Like的發問</li>
    </ul>
);
 
//這是錯誤的寫法
root.render(
    <ul class="menu">
        <li class="menu-item">Like的發問</li>
    </ul>
);

5. style 變為一物件、屬性名稱規則改用駝峰法(用大寫區隔)、屬性的值變成字串

const menuItemStyle = {
    marginBottom: '7px',
    paddingLeft: '26px',
    listStyle: 'none',
};
 
root.render(
    <ul className="menu">
        <li className="menu-item" style={menuItemStyle}>
            Like的發問
        </li>
    </ul>
);

需要特別注意的是直接在標籤中給 style 值的寫法:

root.render(
    <ul className="menu">
        <li
            className="menu-item"
            style={{ marginBottom: '7px', paddingLeft: '26px', listStyle: 'none' }}
        >
            Like的發問
        </li>
    </ul>
);

這裡之所以會有兩層大括號,是因為外面那層括號代表 style 要被賦予的值會是 javascript 語法,裡面的括號則表示物件型態。

6. 元素 Array 會被自動展開

在下面的程式中,menuItemWording 是一個 array,React 就自動展開它裡面的元素並顯示。

import React from 'react';
import { createRoot } from 'react-dom/client';
 
const root = createRoot(document.getElementById('root'));
 
let menuItemWording = ['Like的發問', 'Like的回答', 'Like的文章', 'Like的留言'];
let menuItemArr = menuItemWording.map(wording => <li className="menu-item"> {wording}</li>);
 
root.render(<ul className="menu">{menuItemArr}</ul>);

7. JSX 的 JS 表示式中,可以用布林值比較來決定是否顯示元素

在 Javascript 中有個特別的布林值比較語法。像是以下 code 代表如果condition為 true 時,回傳後面的 1

const condition = true;
const data = condition && 1;
 
console.log(data);
// 印出 1

我們可以將這個語法利用在 JSX 中,讓程式碼更簡潔。例如在以下程式碼中,你會發現按鍵並沒有被顯示

root.render(
    <ul className="menu">{false && <button>我是按鍵</button>}</ul>
);

換成這個就會顯示了

root.render(
    <ul className="menu">{true && <button>我是按鍵</button>}</ul>
);

也就是剛剛的程式碼其實等於

const menuItemFactory = () => {
    if (true) return <button>我是按鍵</button>;
};
 
root.render(<ul className="menu">{menuItemFactory()}</ul>);

8. 所有原生的屬性名稱改為駝峰法命名

例如,onclick 變成了 onClick。

const handleClick = event => {
    console.log(event.target.value);
};
 
root.render(
    <ul className="menu">
        <button value={87} onClick={handleClick}>
            我是按鍵
        </button>
    </ul>
);

有關其他輸入元素的控制方法,我們會在後面的章節提到。

React 17 之後

使用 JSX,再讓 babel 轉成React.createElement的做法在開發上雖然比以前方便很多,但也遇到了兩個問題:

  1. 為了要讓 Babel 知道要轉成React.createElement,每次在 React 元件的 JS 檔中都要import React from 'react'
  2. 有一些改善效能、簡化的語法會因為React.createElement而不能使用。

為了解決這兩個問題,在 2020 年推出的 React 17 中,讓 babel 能夠在編譯時,只要遇到 JSX 就會知道要轉成一個新的函式 - jsx()。簡單來說,以後我們就不用在開頭一直import React from 'react'了!

jsx('h1', { children: 'Hello world' });
⚠️

在目前(2024/02)的 React 中,預設還是用原本的編譯方式。如果你現在就想體驗這個功能,要對打包工具新增一些設定,詳情可以參考React 官網 (opens in a new tab)

// If you are using @babel/preset-react
{
    "presets": [
        // 把@babel/preset-react改成下面這樣
        ["@babel/preset-react", { "runtime": "automatic" }]
    ]
}