React 基礎


Posted by ericcch24 on 2021-05-16

React 中一定會用到的 JavaScript 語法

解構賦值(Destructuring assignment)

  • 物件
// 一個帶有非常多資料的物件
const product = {
  name: 'iPhone',
  image: 'https://i.imgur.com/b3qRKiI.jpg',
  description:
    '全面創新的三相機系統,身懷萬千本領,卻簡練易用。',
  brand: {
    name: 'Apple',
  },
  aggregateRating: {
    ratingValue: '4.6',
    reviewCount: '120',
  },
  offers: {
    priceCurrency: 'TWD',
    price: '26,900',
  },
};
/* 物件的解構賦值 */

// 自動產生名為 name 和 description 的變數
// 並把 product 物件內的 name 和 description 當作變數的值
const { name, description } = product;

console.log(name);         // iPhone
console.log(description);  // 全面創新的三相機系統,身懷萬千本領,卻簡練易用。...

如果要取得 offers 物件內的 price:

const {
  offers: { price },
} = product;

console.log(price);    // 26,900
console.log(offers);   // ReferenceError: offers is not defined

如果同時需要建立 offersprice 這兩個變數

const { offers } = product;    // 透過解構賦值先從 product 取出 offers
const { price } = offers;      // 透過解構賦值再從 offers 中取出 price

console.log(price);   // 26,900
console.log(offers);  // { priceCurrency: 'TWD', price: '26,900' }
  • 陣列
const mobileBrands = [
  'Samsung', 'Apple', 'Huawei', 'Oppo',
  'Vivo', 'Xiaomi', 'LG', 'Lenovo', 'ZTE'
];
/* 陣列的解構賦值 */

// 自動建立名為 best、second、third 的變數
// 並把 mobileBrands 陣列中的第一、第二和第三個元素當作變數的值帶入
const [best, second, third] = mobileBrands;

console.log(best);     // Samsung
console.log(second);   // Apple
console.log(third);    // Huawei

展開語法和其餘語法(Spread Syntax/Rest Syntax)

  • 展開語法(spread syntax):把「展開語法」的 ... 當作「解壓縮」的概念,就是把原本的物件,解開來,再放進去新的物件裡面,同時還可以添加一些新的屬性。
// 定義一個物件
const mobilePhone = {
  name: 'mobile phone',
  publishedYear: '2019',
};

原本有的屬性內容會被覆蓋,沒有的會直接加進去。

/* 展開語法(spread syntax) */

const iPhone = {
  ...mobilePhone,
  name: 'iPhone',
  os: 'iOS',
};

console.log(iPhone);  // { name: 'iPhone', publishedYear: '2019', os: 'iOS' }
  • 其餘語法(rest syntax):如果說展開語法像是「解壓縮」,那麼其餘語法就像是「壓縮」。它可以把在解構賦值中沒有被取出來的物件屬性或陣列元素都放到一個壓縮包裡。

以最上面 iphone product 的陣列為例,other 就會是 product 物件中,扣除掉 namedescription 屬性後的所有其餘資料:

/* 物件解構賦值時使用其餘語法 */
const { name, description, ...other } = product;
console.log(other);

// {
//   image: 'https://i.imgur.com/b3qRKiI.jpg',
//   brand: { name: 'Apple' },
//   aggregateRating: { ratingValue: '4.6', reviewCount: '120' },
//   offers: { priceCurrency: 'TWD', price: '26,900' }
// }

陣列:

/* 陣列解構賦值時使用其餘語法 */

const mobileBrands = [
  'Samsung', 'Apple', 'Huawei', 'Oppo',
  'Vivo', 'Xiaomi', 'LG', 'Lenovo', 'ZTE'
];

// 變數名稱不一定要取名為 other
const [best, second, third, ...other] = mobileBrands;
console.log(other);   // [ 'Oppo', 'Vivo', 'Xiaomi', 'LG', 'Lenovo', 'ZTE' ]

參考資料


React 核心概念

之前在用 jQuery 寫網頁時,在操作網頁之後需要做三件事:更新畫面、更新資料、儲存資料,而React 可以讓我們==只需要更新與儲存資料,再依據資料目前的狀態(state)來 render 畫面==。
這樣就不需要在更新資料之餘還要考慮畫面怎麼更新,因為畫面是從資料的狀態產生的。

資料 state => 畫面UI
state changed => 畫面 re-render

Component 組件

把畫面上的東西分成幾個組件區塊,程式碼就可以不同組件分成不同函式寫,重點是要盡量讓每個組件有重用性(重複使用)。

畫面永遠由 state(狀態) 產生

幾乎不會改到畫面,只會改資料。

當每次資料更動時,會先將畫面清空再依據當時的 state 重新渲染資料,這樣就可以確保資料跟畫面 UI 的一致,但每次資料更動完(假設資料超多而現在只要改一個小地方)就要整個畫面重新 render 很浪費效能,所以==React 的 render 機制會比較現在的 state 與上一次的 state 之間的差別來處理需要更動的部分。==

React 的渲染機制(Reconciliation)與 Virtual DOM


Virtual DOM 實際上的作法就是用物件來描述 DOM 的結構,在 DOM 的節點需要更動時,不直接修改 DOM,而是透過 diff 演算法比較 Virtual DOM 修改前與修改後的樹狀結構,然後批次更新真實 DOM 中的節點。

  • 為什麼要使用 Virtual DOM?
    操作 DOM 的成本是昂貴的,現今網頁的 DOM Element 數量都頗大,瑣碎頻繁地更新容易成為效能瓶頸。如果不直接操作 DOM,而是將頁面上的 DOM 經 parse/traversal 為 JavaScript 物件暫存在某個地方,對這些物件操作,然後再更新到 DOM 上,想必會比直接操作 DOM 來得快速許多。因此,使用 Virtual DOM 的最大好處就是提升效能 。

參考資料:Virtual DOM | 為了瞭解原理,那就來實作一個簡易 Virtual DOM 吧!


React 環境建置 :create-react-app

npx create-react-app my-app
cd my-app
npm start

什麼是 JSX

在 JSX 的加持之下,讓開發者可以把 JavaScript 內的用法與程式邏輯,直接套用到 HTML 的元素上,就是一個「強化版 HTML 」的概念!

在 JavaScript 的地方則是使用了 ReactDOM.render 這個語法,在 ReactDOM.render() 的第一個參數放的就是 JSX 的內容,這裡看起來其實就和直接放入 HTML 一樣;第二個參數則是說明這個 JSX 的內容要被放到原本 HTML 中的哪個位置內,這裡就是放到 #root 這個元素上:

<!-- HTML -->
<div id="root"></div>
// JavaScript
ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

透過 JSX 等於我們可以直接把 HTML 放到 JavaScript 中去操作,不再需要先用 querySelector 去選到該元素後才能換掉,而是可以在 HTML 中直接帶入 JavaScript 的變數。


JSX 會幫忙 escape,所以不用擔心 XSS 的問題

==但是在連結的 tag 放 input 內容時要特別注意不會 escape 到,需要加上windows.encodeURIComponent()==
例如:<a href={windows.encodeURIComponent(todo.content)}>click me</a>


補充:在 JSX 中,當開始和結束的標籤(tag)之間沒有任何內容的時候,也就是該標籤內沒有子層元素時,可以把它自我關閉(self closing tag)起來,也就是在開頭的 HTML 標籤最後加上 / 即可,結尾的 HTML 標籤即可移除,舉例來說 <div></div>,因為開頭和結尾的 HTML 標籤之間沒有任何內容,因此在 JSX 中會變成 <div />

<!-- 原本的 HTML -->
<div class="chevron chevron-down"></div>

<!-- JSX 中變成 -->
<div class="chevron chevron-down" />

總結在 HTML 切換成 JSX 過程中需要留意的:

  • 把 HTML 中的 class 屬性改成 className
  • inline-style 的 CSS 屬性命名要用小寫駝峰
  • 當標籤內沒有內容時,可以讓該元素自行關閉(self-closing)
  • 一個 JSX 最多只能有一個外層元素
// ❌ 這是不被允許的
const Counter = () => (
  <div class="container">
    <!-- ... -->
  </div>
  <div class="other-container">
    <!-- ... -->
  </div>
);

在 JSX 使用 inline-style(行內樣式)

在 JSX 中可以使用 {} 來帶入變數,當我們想要撰寫 inline-style 時,就可以在 <div style={} >{} 中放入物件,==物件的屬性名稱會是 CSS 的屬性,命名會用「小寫駝峰」來表示==;屬性值則是 CSS 的值,具體的寫法會像這樣:

// 定義 inline-style 行內樣式
const someStyle = {
  backgroundColor: white,
  fontSize: '20px',          // 也可以寫 20,引號和 px 可以省略
  border: '1px solid white',
  padding: 10,               // 省略 px,樣式會自動帶入單位變成 '10px'
}

// 在 style 中帶入物件,即可撰寫出 inline-style
const SomeElement = (
  <div style={someStyle} />
)

有時候也會直接把 inline-style 這個物件寫在 style={}{}

const Counter = (
  // ...
  <div
    className="number"
    // 直接把定義 inline-style 的物件,放到 style={} 的 {} 內
    style={{
      color: '#FFE8E8',
      textShadow: '2px 2px #434a54',
    }}
  >
    256
  </div>
  // ...
);

建立 react 組件

React 組件的命名是使用大寫駝峰,因此首字需要大寫,同時因為這個組件單純只是要回傳 JSX 而沒有要做其他處理,所以可以在箭頭函式的 => 後直接回傳 JSX 即可,像這樣:

const UnitControl = () => (
  <div className="unit-control">
    <div className="unit">Mbps</div>
    <span className="exchange-icon fa-fw fa-stack">
      <i className="far fa-circle fa-stack-2x" />
      <i className="fas fa-exchange-alt fa-stack-1x" />
    </span>
    <div className="unit">Mb/s</div>
  </div>
);
// 建立一個名為 Counter 的 React 組件
const Counter = () => {
  return (
    <div className="container">
      <div className="chevron chevron-up" />
      <div className="number">256</div>
      <div className="chevron chevron-down" />
    </div>
  );
};

將寫好的組件顯示出來

const Counter = () => (
  <div className="container">
    <div className="chevron chevron-up" />
    <div className="number">256</div>
    <div className="chevron chevron-down" />
  </div>
);

// 使用 <Counter /> 來帶入 React 組件
ReactDOM.render(<Counter />, document.getElementById('root'));

React 基本 component 應用

function Title() {
  return (
    <h1>Hello</h1>
  )
}

function Description({ children }) {
  return (
    <p>
      {children} 
      // 將內容以 props (上面解構的 children)參數傳入
    </p>
  )
}

function App() {
  return (
    <div className="App">
      <Title /> // JSX 語法
      // 沒有參數就可以直接用單個 tag
      <Description>
        cool man 
        poop
        // 引入 Description({ children }) 函式的格式
      </Description>
    </div>
  );

React 中的命名慣例

React 的「組件名稱」會以大寫駝峰的方式來命名,也就是首字母大寫,例如, Counter,若該名稱由多個單字組成,則把每一單字的第一個字大寫,例如,AdminHeaderPaymentButton。如果沒這麼做的話,React 會把它當作一般的 HTML 元素處理,並跳出錯誤提示。

其他像是 HTML 中的屬性、CSS 樣式屬性或一般的函式來說,則會遵行 JavaScript 以小寫駝峰來命名變數的慣例,例如在 classNamemaxLengthbackgroundColor 等等。

<input type="text" maxlength="10" /> 為例,在 React 的 JSX 中需要把 maxlength 改成 maxLength,不然一樣會拋出錯誤:


在 React 組件中綁定事件監聽器

<div className="chevron chevron-up" onClick={/* ... */} />
const { useState } = React;

const Counter = () => {
  const [count, setCount] = useState(256)
  return (
    <div className="container" style={shadow}>
      {console.log("render", count)}
      <div className="chevron chevron-up" 
        onClick={() => {
          setCount(count + 1)
        }}
       />
      <div className="number">{count}</div>
      <div className="chevron chevron-down" 
        onClick = {() => {
          setCount(count - 1)
        }}
      />
    </div>
  );
}

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

其中可以把 onClick 的動作抽出來另外寫一個 function 並帶入參數

const Counter = () => {
  const [count, setCount] = useState(5);

  const handleClick = (type) => {
    if (type === 'increment') {
      setCount(count + 1);
    }
    if (type === 'decrement') {
      setCount(count - 1);
    }
  };

  return (
    // ...
  );
};

但要特別留意函式後加上小括號會直接呼叫該函式,寫成這樣 onClick={handleClick('increment')} 是錯的,當我們寫 onClick={handleClick('increment')} 時,我們預期的的是「當使用者點擊按鈕時,會去執行 handleClick('increment') 這個方法」。但實際上,因為 handleClick 後面直接加上了小括號 ('increment'),因此當 JavaScript 執行到這裡的時候,這個 handleClick 函式就已經被執行了!

正確寫法要把 handleClick() 包在一個函式中 () => {handleClick('increment')}
這樣的話,畫面渲染的時候 handleClick 就不會馬上被執行,而是在使用者點擊按鈕的時候才會去執行 () => handleClick('increment') 這個函式。

綁定 onChange 事件

若要監控使用者在 <input /> 欄位中輸入了什麼,可以使用 onChange 事件,可以在 <input /> 內加上 onChange,在後面的 {} 內透過 console.log 來看看是否會觸發此事件,像是這樣:

{/* ... */}
  <div className="flex-1">
    <div className="converter-title">Set</div>
    <input
      type="number"
      onChange={() => console.log("onChange")}
      className="input-number"
      min="0"
    />
  </div>
{/* ... */}

提示:在 React 中,常使用 handle 當作事件處理器的開頭,例如 onClick 對應到 handleClick,onChange 對應到 handleChange。


將資料從父層組件傳遞到子層組件

在 React 中把父層組件的資料狀態傳遞到子層組件的方式非常簡單,只需要透過類似 HTML 屬性的方式放在該組件的標籤內就可以了,接著在子層組件的參數中,就可以透過 props 把傳入的資料取出,像是這樣:

// STEP 2: 在該 component 內可以透過參數 props 取得傳入的資料
function ChildComponent(props) {
  const { firstName, lastName } = props;
  return <h1>Hello, {firstName} {lastName}</h1>;    // Hello, Aaron Chen
}
// 甚至更精簡到連 props 都不命名了,直接取出來用:
// 透過解構賦值直接在「函式參數的地方」把需要用到的變數取出
function ChildComponent({ firstName, lastName }) {
  return <h1>Hello, {firstName} {lastName}</h1>;    // Hello, Aaron Chen
}

// STEP 1: 將資料透過 html 屬性的方式傳入 component 內
const element = <ChildComponent firstName="Aaron" lastName="Chen" />;

React 的迴圈

在 React 中,當我們要做重複渲染多個組件時,最常使用到的是透過陣列的 map 方法,因為 map 這個方法會有回傳值,所以可以直接在 JSX 中使用。

實際的做法會像這樣:

  1. 透過 Array.from() 先建立一個帶有 n 個元素的陣列
  2. 在 JSX 中將這個陣列使用 map 方法,並且每次都回傳 <Counter /> 元素

透過 Array.from 一次產生帶有 n 個元素的陣列

在建立帶有多個元素的陣列時,經常會使用到 Array.from() 這個方法,下面列出常用的方式:

// 產生元素數目為 10,元素值都為 undefined 的陣列
Array.from({ length: 10 });    // [undefined, undefined, ..., undefined]

// 產生元素數目為 10,元素值為 0 ~ 9 的陣列
Array.from({ length: 10 }, (_, index) => index); // [0, 1, 2, ..., 8, 9]

// 註: v, i 可帶換任何字
Array.from({length: 5}, (v, i) => i); // [0, 1, 2, 3, 4]

透過陣列的 map 方法來執行迴圈

// ...
ReactDOM.render(
  <div
    style={{
      display: 'flex',
      flexWrap: 'wrap',
    }}
  >
    {/* STEP 2: 使用 map 產生多個 <Counter /> */}
    {counters.map((item) => (
      <Counter />
    ))}
  </div>,
  document.getElementById('root')
);

React 中 的 input 的值

有分 controller component 跟 uncontroller component

controller component: 建立 onChange 事件來改變 value 的 set


function App() {

  const [value, setValue] = useState('') // value 初始值為空字串

  const handleInputChange = (e) => {
    setValue(e.target.value) 
    // 在 input 打字時,重新 render 輸入的 value
  }


  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
      }
    </div>
  );
}

React 中 todo list 的新增、編輯、刪除

==因為 setState 不可修改原本的東西,所以要透過產生新的陣列來新增、編輯、刪除==

  • 新增用解構語法,再放入值
  • 編輯用 map
  • 刪除用 filter
function App() {
  const [todos, setTodos] = useState([
    {id: 1, content:'abc', isDone: true},
    {id: 2, content:'not done', isDone: false}
  ])

  const [value, setValue] = useState('')
  const id = useRef(3)

  // 新增用解構語法,再放入值
  const handleButtonClick = () => {
    setTodos([{
      id: id.current,
      content: value
    }, ...todos])
    setValue('')
    id.current++
  }

  const handleInputChange = (e) => {
    setValue(e.target.value)
  }

  // 修改用 map
  const handleToggleIsDone = id => {
    setTodos(todos.map(todo => {
      if (todo.id !== id) return todo
      return {
        ...todo,
        isDone: !todo.isDone
      }
    }))
  }


  // 刪除用 filter
  const handleDeleteTodo = id => {
    setTodos(todos.filter(todo => todo.id !== id)) // 把不是 id 的保留,反之就是去掉 id
  }

  return (
    <div className="App">
      <input type="text" placeholder="todo" value={value} onChange={handleInputChange}/>
      <button onClick={handleButtonClick}>Add todo</button>
      {
        todos.map((todo) => <TodoItem key={todo.id} todo={todo} handleDeleteTodo={handleDeleteTodo} />)
      }
    </div>
  );
}

React 的邏輯運算

在 JSX 中的 {} 內只能放入表達式(expressions),而不能寫入像是 if...else... 這種陳述句(statement),因此在 React 中很常時候都會使用邏輯運算子這種語法,例如 &&|| 這種語法稱作「邏輯運算子(Expressions - Logical operator)。

  • || 邏輯上是「或(or)」的意思,在 JavaScript 中常常被當做定義變數的預設值來使用,假設寫 a || b 的話,意思就是當 a 為 false(為假)時就用 b,當 a 為 true(為真)時就直接用 a。

|| 簡單來說,就是當 || 前面的值為 false(假)時,就取後面的那個當值。

  • 至於 && 則反過來。當寫 a && b 時,當 a 為 true(為真)時,就拿後面的 b,否則拿 a。
    && 簡單來說,就是當 && 前面的值為 true 時,就取後面的那個當值。

在 React 中,除了使用 && 和 || 來進行條件渲染之外,當要做到「若 ... 則 ...,否則...」的這種功能時,則會使用三元判斷式(ternary operator),也就是 ... ? ... : ... 的這種寫法。


React 特別的事件機制

例如在 button 上面放的 click 事件,react 會用事件代理機制把 eventListener 綁在最上層的 root,而不是放在 button 上,所以在點擊的時候監聽的事件是代理在放置 JSX 內容的 HTML 節點上,如下面的 root。

<div id="root"></div>
ReactDOM.render(
  <ThemeProvider theme={theme}>
    <App />
  </ThemeProvider>,
  document.getElementById("root")
);

Function component vs Class component

* How Are Function Components Different from Classes?

額外補充

  • 優化時間呈現:可以使用瀏覽器原生的 Intl 這個方法,這個方法的全名是 Internationalization API,它可以針對日期、時間、數字(貨幣)等資料進行多語系的呈現處理,相當方便,有興趣的話可以進一步參考 MDN 官方文件的說明。

  • 透過陣列的 reduce 方法搭配 includes 可以拿取較深且元素數量大的資料出來

const weatherElements = locationData.weatherElement.reduce(
  (neededElements, item) => {
    if (["WDSD", "TEMP", "HUMD"].includes(item.elementName)) {
      neededElements[item.elementName] = item.elementValue;
    }
    return neededElements;
  },
  {}
);
  • 優化我的 code: prettier
    Prettier - Code formatter
    安裝重點:要在根目錄建立一個 .prettierrc.json 並寫入規則
    ###### tags: Week21

#week21







Related Posts

展開運算子(Spread Operator) & 其餘運算子(Rest Operator)

展開運算子(Spread Operator) & 其餘運算子(Rest Operator)

JavaScript: Object-oriented JavaScript, Prototype Chain & This

JavaScript: Object-oriented JavaScript, Prototype Chain & This

謂詞邏輯(Predicate Logic)

謂詞邏輯(Predicate Logic)


Comments