ReactJS 教學系列影片筆記

前言

本篇是參考 ReactJS Tutorial 的重點摘錄學習筆記。內容著重於 React 16.8 以前沒有 Hooks 時,以 Class Component 為主的教學。

閱讀前具備知識:

  • 基礎網頁前端 (HTML、CSS、JS) 知識
  • 基礎 ReactJS 知識

5 關於 export, import 和命名

使用 export default 輸出的話,import 的時候可以自訂名稱。
使用 export 輸出的話,import 的時候要用一樣的名稱,並且用 {} 包住。例:import { Greet } from './component/test';

更多詳細內容請參考:JavaScript ‪Module Cheatsheet

9 props.children

props 是 read-only 變數。

  • props - Functional Component
  • this.props - Class Component

可以將客製化的 html 內層,寫在 component 外,但是在component內用 props.children 可以抓到:

1
2
3
4
5
6
7
8
9
10
11
// Person component
function Person (props){
return(
<div>
<h2>{props.greet}</h2>
{props.children}
</div>
);
}

export default Person;
1
2
3
4
5
6
7
8
// 引用 Person component 時,客製化 html 內層
render(){
return(
<Person greet={"hello"}>
這裡寫的東西,Person component 可以用 props.children 抓到,並放置在該位置。
</Person>
);
}

11 setState 有三種寫法

props 是 read-only 變數。

  • useState Hook - Functional Component
  • this.state - Class Component
1
2
3
4
// 一、傳入一個物件當作參數
this.setState({
num: this.state.num + 1,
});
1
2
3
4
5
6
7
8
9
10
// 二、setState 模擬異步行為
// 因為 React.js 並不會馬上修改 state,而是把這個物件放到一個更新隊列裡面。
// 稍後才會從隊列當中把新的狀態提取出來合併到舊的 state 當中,然後再觸發組件更新。
// 因此想要在 setState 後執行的程式,以 callback 的方式當作第二個參數傳入,記得使用匿名/箭頭函示
// 合成事件 (onClick) 和 callback 中是異步的,在原生事件和 setTimeout 中是同步的。
this.setState({
num: this.state.num + 1,
}, () => {
console.log(this.state.num);
});
1
2
3
4
5
6
7
8
9
// 三、批量更新
// React.js 內會把 JavaScript 事件循環中的消息列隊的同一個消息中的 setState
// 都進行合併以後再重新渲染組件。
// 也就是寫了很多行類似的會合併執行
// 寫了很多行一樣的「只會執行最後一次」,這個會有問題,用 prevState 解決
// 原生事件和 setTimeout 中不會批量更新
this.setState((prevState[, prevProps]) => ({
num: prevState.num + 1,
}));

參考資料:
React 中 setState() 为什么是异步的
組件的 state 和 setState

13 Event Handling

將事件處理器 Event Hander 的變數放在 {} 中。

1
2
3
<button onClick={這邊是要傳入一個 function/method 參數作為事件處理器}>
1
</button>

不要直接在 {} 中呼叫函式,錯誤寫法:

1
2
3
<button onClick={clickHandler()}>
1
</button>

Functional Component 中,事件處理器不需要綁定 this,因為沒有 this。

14 Binding Event Handlers

Class Component 中,當 method 或事件處理器 (clickHandler) 中會需要用到 class 中的其他 property or method (例如:this.state and this.methed) 時,就需要將該 state 或 method 的 thisclass 的 this 綁定,確保 this 表示的是 class 而不是 tag。

1
2
3
4
5
6
// 一、將 bind 寫在 render() 事件的參數中
// 缺點:重新 render 時就會產生新的事件處理器 (event handler),效能不佳
// 有很多巢狀子元件的話,一樣效能不佳
<button onClick={this.clickHandler.bind(this)}>
State:點我計算次數
</button>

錯誤寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 以下這個 method 還沒和 class 綁在一起
// 因此不知道它的所屬物件 (this) 是誰
// 在裡面使用 console.log(this); 也就會不知道 this 是誰
clickHandler(){
console.log(this); // undefined

// 以下會出現錯誤,因為以為 this 是 button tag
// 但是 tag 裡面沒有定義 setState
this.setState({
//...
});
}

// render method 中,沒有綁定
<button onClick={this.clickHandler}>
State:點我計算次數
</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 二、render() 事件的參數中使用箭頭函示,再呼叫 clickHandler
// 這種寫法變成事件處理器是 () => this.clickHandler()
// 缺點:如第一個
// 優點:因為有()方便傳值
<button onClick={() => this.clickHandler()}>
State:點我計算次數
</button>

// 可以改寫成如下,也就是
// 呼叫 clickHandler 然後回傳它的值,所以記得要加()
<button
onClick={() => {
return this.clickHandler();
}}
>
State:點我計算次數
</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 三、在建構子中綁定,render() 事件中參數簡化
constructor(props){
super(props)

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

render(){
return (
<button onClick={this.clickHandler}>
State:點我計算次數
</button>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
// 四、使用箭頭函示,將 clickHandler 設為 class 的 property
clickHandler = () => {
//....
}

render(){
return (
<button onClick={this.clickHandler}>
State:點我計算次數
</button>
)
}

15 Methods as props

child component 如何和 parent component 溝通,或將值回傳至 parent component:

1
2
3
4
5
6
7
8
// parent component
greetParent = (childName) => {
alert(`Hello Parent from ${childName}`);
}

render(){
return <ChildComponent greetHandler={this.greetParent}/>
}
1
2
3
4
5
6
7
8
9
// child component
// html標籤中,事件的參數使用箭頭函示,再呼叫父層傳入的方法 greetHandler
// 並將參數傳入,等同於將值回傳至父層
// 優點:因為有()方便傳值
function ChildComponent (props){
return <button onClick={() => props.greetHandler("child")}>Greet Parent</button>
}

export default ChildComponent;

16 Conditional Rendering 四種條件渲染

JSX 是語法糖,只允許函式呼叫和物件格式,不允許 if/else 寫在裡面,只能將 if/else 寫在 return (JSX) 外面。

但是三元運算符 (ternary conditional operator) 可以寫在 JSX 裡面。

1
2
3
4
5
6
7
8
9
10
// 短路求值 short circuit evaluation

// 當 this.state.isLoggedIn 為 true 時,才會出現 <div>welcome</div>

// this.state.isLoggedIn 為 false 時
// (this.state.isLoggedIn && <div>welcome</div>) 為 false
// 才會出現 <div>Guest</div>
render(){
return ((this.state.isLoggedIn && <div>welcome</div>) || <div>Guest</div>)
}

19 Index as Key Anti-pattern 可以使用 index 當 key 的條件

  1. 陣列裡的 items 沒有 unique ID。
  2. 陣列是靜態的,不會改變。
  3. 陣列不會被重新排列或過濾 (filter)。

20 Styling and CSS Basics

  1. CSS stylesheets (className)
  2. Inline styling (記得不能用 font-size 要用 fontSize)
  3. CSS Modules (react-script 2.0 以上版本)

22 Component Lifecycle Methods 三階段 + 錯誤處理

React Lifecycle Methods Diagram

  • Mounting
    • constructor()
    • static getDerivedStateFromProps()
    • render()
    • componentDidMount()
  • Updating
    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()
  • Unmounting
    • componentWillUnmount()
  • Error Handling
    • static getDerivedStateFromError()
    • componentDidCatch()

23 Component Mounting Lifecycle Methods




24 Component Updating Lifecycle Methods







25 Fragments (without adding extra nodes to the DOM)

每次要 return 時,只能回傳一個標籤組,但是用 <div></div> 又會多一組額外的標籤,可以改用 React.Fragment,好處是用 array.map render 時可以加 key。

React.Fragment 可以用空標籤 (<></>) 代替,缺點是不能加 key。

1
2
3
4
5
6
return(
<React.Fragment>
<h1>Tile</h1>
<p>Here is sentence.</p>
</React.Fragment>
);

26 Pure Components

PureComponent 是一種 Component,繼承 React.PureComponent,如果 props 和 states 都沒有改變,預防不必要的 re-render,內部已經幫我們寫好 shouldComponentUpdate,大概長得如下:

1
2
3
4
shouldComponentUpdate(prevProps, prevState){
return !(prevProps.props1 === this.props.props1) // for all props
||!(prevState.state1 === this.state.state1) // for all state
}

主要是進行淺比較 Shallow Comparison (SC)

  • Primitive types
    • a 和 b 有一樣的值和型態,ex:
    1
    2
    3
    let a = "Jenifer";
    let b = "Jenifer";
    // return true
  • Complex types 物件、陣列
    • a 和 b 有一樣的 reference,ex:
    1
    2
    3
    4
    5
    let a = [1, 2, 3];
    let b = [1, 2, 3];
    let c = a;
    // a (SC) b return false
    // a (SC) c return true

因為是比較 reference,所以當陣列用 push 新增一個 item 時,沒有改變 reference,PureComponent 會感覺不到,不會重新渲染,這不是我要的。使用 PureComponent 時,記得回傳一個新物件或陣列。

確保 PureComponent 的子層也都是 PureComponent,才不會有非預期的行為。

將 render props 與 React.PureComponent 一起用時
因為 每一次的 render prop 都是新的值,淺比較時永遠得到 false -> 會重新渲染
解決方法:在上一層裡面定義靜態 render prop

27 memo

react and react-dom 16.6.0 以上的版本,才有此功能。

React.memo is a higher order component (HOC). 它是 function component 版本的 PureComponent,如果 props 和 states 都沒有改變,預防不必要的 re-render

因為是 HOC 所以用法跟 PureComponent 不一樣,如下:

1
2
3
4
5
6
7
import React from "react";

function MemoComp(){
// ...
}

export default React.memo(MemoComp);

28 Refs 直接 access to DOM

將 Refs 和某個 element 綁在一起,即可使用 Refs 直接取得 DOM,呼叫其 value 和 method。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一、在建構子中使用 React.createRef()
constructor(props) {
super(props);
this.inputRef = React.createRef();
}

render() {
return (
<div>
<input type="text" ref={this.inputRef} />
<input type="submit" value="送出" onClick={this.handleClick} />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
// 二、React.createRef() 寫在建構子外
inputRef = React.createRef();

render() {
return (
<div>
<input type="text" ref={this.inputRef} />
<input type="submit" value="送出" onClick={this.handleClick} />
</div>
);
}

this.inputRef.current 可以直接取到 DOM 元素,若是使用 console.log(this.inputRef.current) 會看到如下左圖:

類似 console.dir(element); 如上右圖。

關於 Refs 舊版的寫法,用到 callback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 一、在建構子中使用 React.createRef()
constructor(props) {
super(props);
this.cbRef = null;
this.setCbRef = element => {
this.cbRef = element; // 也就是 <input>
};
}

componentDidMount(){
if(this.cbRef){ // 確認 this.cbRef 不是 null,再執行
this.cbRef.focus();
}
}

render() {
return (
<div>
<input type="text" ref={this.setCbRef} />
<input type="submit" value="送出" onClick={this.handleClick} />
</div>
);
}

29 Refs with Class Components 從父層呼叫子層的 method (一)

Refs 只能和 element 和 class component 綁在一起。Refs 不能和 functional component 綁在一起。

this.inputRef.current 可以直接取到 DOM 元素,可以利用這個特性,從父層呼叫子層的 method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Parents component
import React from "react";
import Input from "./Input";

class FocusInput extends React.Component{

componentRef = React.createRef();

handleClick = () => {
// this.componentRef.current 是 <Input>
// 而 <Input> 實際上是一個 class,有自己的方法
// focusInput 定義在 child component 中
this.componentRef.current.focusInput();
}
render(){
return (
<div>
<Input ref={this.componentRef}/>
<button onClick={this.handleClick}>Click to focus</button>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Child component
class Input extends React.Component{

inputRef = React.createRef();

focusInput(){
this.inputRef.current.focus();
}
render(){
return (
<input type="text" ref={this.inputRef}/>
);
}
}

30 Forwarding Refs 從父層呼叫子層的 method (二)

使用 forwardRef,將父層中的 Refs 直接指向「原生的 component」,另類的從父層呼叫子層的 method。

只有在子層中使用 forwardRef 的寫法,才能呼叫 Refs。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Parents component
import React from "react";
import Input from "./Input";

class FocusInput extends React.Component{

componentRef = React.createRef();

handleClick = () => {
// this.componentRef.current 是子層中的 <input>
this.componentRef.current.focus();
}
render(){
return (
<div>
<Input ref={this.componentRef}/>
<button onClick={this.handleClick}>Click to focus</button>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Child component
// 不能直接使用下面這種普通的 function 寫法
// function Input(){
// return(
// <div><input type="text"/></div>
// );
// }

// 在 forwardRef 的參數中使用「箭頭函式」,且 ref 是第二參數
// ref 只有在 forwardRef 的寫法中,才能在子層中獲得
// 其他時候,想在子層中呼叫 ref 這個參數,都無法呼叫
const Input = React.forwardRef((props, ref) => {
return (
<div>
<input type="text" ref={ref}/>
</div>
);
});

31 Portals

Portals 提供了一種很好的將子節點渲染到父组件以外的 DOM 節點的方式。

如果今天想要製造一個額外的 popup 或提示視窗,UI 想要不被父層的 css 限制,就不能將該 component 放進

1
2
3
4
5
6
<div id="root">
<div class="App">平常使用的這個位置</div>
</div>

<!-- 在 root 下再創造一個 div,將 popup 或提示視窗放在這裡 -->
<div id="portal-root"></div>
1
2
3
4
5
6
7
8
9
10
import React from "react";
import ReactDom from "react-dom"; // 記得引入 react-dom

// 使用 ReactDOM.createPortal(child, container) 要傳入兩個參數
function PortalDemo(){
return ReactDom.createPortal(
<h1>Portal Demo</h1>,
document.getElementById("portal-root")
);
}

另外,通過 Portals 進行事件冒泡

儘管 portal 可以被放置在 DOM 樹的任何地方,但在其他方面其行為和普通的 React 子節點行為一致。如上下文特性依然能夠如之前一樣正確地工作,無論其子節點是否是 portal。只要考慮 portal 存在於 React 樹中的位置,而不用考慮其在 DOM 樹中的位置。

如果今天有一個 portal 組件被放在DOM 樹 #modal-root 中,而 React 樹中被放在 #app-root 底下。

#app-root 裡的父層能夠捕獲到 portal 組件冒泡上来的事件。

1
2
3
4
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>

32 Error Boundary

Error Boundary 是一種 component,可以捕捉被 ErrorBoundary component 包圍住的所有子層在渲染期間、生命週期方法內和建構子內發生的錯誤。並且回傳一個錯誤訊息的 UI。

這個可以用在電商的網站,如果僅一個商品或組建出錯,我們不會希望全部的其他 999 件商品,整個網站都無法顯示,使用 Error Boundary 可以產生一個錯誤訊息的 UI。

創造一個 class 的 ErrorBoundary 組件的方法就是,在 class 中呼叫 static getDerivedStateFromError(error){}componentDidCatch(error, info){} 其中一個或兩者。

1
2
3
<ErrorBoundary>
<Hero />
</ErrorBoundary>
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
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error){
return { hasError: true };
}

componentDidCatch(error, info) {
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}

render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
// 這是被 ErrorBoundary 包起來的子層 <Hero>
return this.props.children;
}
}

ErrorBoundary 放置的位置可以是只包一個組件,或包住全部的組件。但是只要被包住的範圍出現錯誤,被包住的組件就不會被渲染出來,會渲染出 fallback UI。

33 Higher Order Components (Part 1)

使用 HOC 的原因:當有兩個以上的元件,生命週期做的事情相同時,需要寫很多次一樣或類似的程式碼,程式碼不簡約而且有很多重複的部分,這時後就可以利用 HOC 的特性。

最初有人這樣想,將 state 提升到父層,並且將處理事件的 handler 當成 props 傳遞給子層。

但是萬一如果父層和子層差距很遠怎麼辦?需要用到該 state 和 handler 的元件可能散布在 React 樹中各處,因此這個想法某些時候不適用。

34 Higher Order Components (Part 2)

HOC 是一個 function,將 component 作為 argument 傳入,並且回傳一個新的 component,會長得有點像這樣:
const newComponent = HOC(originalComponent)

經過 A HOC 的 component 會有 A 的特性和功能; 經過 B HOC 的 component 會有 B 的特性和功能。

可以寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";

const UpdatedComponent = (OriginalComponent) => {
// 將原本的元件升級後
class NewComponent extends React.Component {
render() {
return <OriginalComponent/>
}
}
// 再回傳
return NewComponent;
}

export default UpdatedComponent;

也可以寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";

function UpdatedComponent(OriginalComponent) {
// 將原本的元件升級後
class NewComponent extends React.Component {
render() {
return <OriginalComponent/>
}
}
// 再回傳
return NewComponent; // 其實 return 還可以再提升到第五行前面,並省略 NewComponent
}

export default UpdatedComponent;

現在來舉個真實的例子,並套用命名慣例:

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
// withCounter.js 
// 這個檔案是「升級配備 HOC function」
import React from "react";

function withCounter(WrappedComponent) {
class WithCounter extends React.Component {
state = {
num: 0,
}

countHandler = () => {
this.setState((prevState) => {
return { num: prevState.num + 1, }
});
}

render() {
return (
<WrappedComponent
num={this.state.num}
countHandler={this.countHandler}
/>
);
}
}
return WithCounter;
}

export default withCounter;
1
2
3
4
5
6
7
8
9
10
11
12
13
// ClickBtn.js
import React from "react";
import withCounter from "./withCounter";

class ClickBtn extends React.Component {
render() {
const { num, countHandler } = this.props;
return <button onClick={countHandler}>Click {num} times</button>;
}
}

// export default ClickBtn; // 原本是這行
export default withCounter(ClickBtn); // 升級版 ClickBtn
1
2
3
4
5
6
7
8
9
10
11
12
13
// HoverTitle.js
import React from "react";
import withCounter from "./withCounter";

class HoverTitle extends React.Component {
render() {
const { num, countHandler } = this.props;
return <h1 onMouseOver={countHandler}>Hover {num} times</h1>;
}
}

// export default HoverTitle; // 原本是這行
export default withCounter(HoverTitle); // 升級版 HoverTitle

35 Higher Order Components (Part 3)

一、記得傳入其餘參數

如果我直接在 App.js 中,想將傳遞的參數寫入 (如下),會出錯!!!!!! this.props.name 會被傳入 withCounter function 中,沒有傳到 ClickBtn 這個 class component 中。

1
2
3
4
5
6
7
8
render() {
return (
<div className="App">
<ClickBtnWithCounter name={"Jenifer"}/>
<HoverTitleWithCounter/>
</div>
);
}

既然 this.props.name 有被傳入 withCounter function 中,修正方法就是在 withCounter (HOC) 中對 WrappedComponent 增加 {...this.props},將其餘的 props 也傳入該元件。

1
2
3
4
5
6
7
8
9
10
// withCounter.js 
render() {
return (
<WrappedComponent
num={this.state.num}
countHandler={this.countHandler}
{...this.props} // 增加這一行
/>
);
}

二、HOC function 可以傳入其他參數 parameters

1
2
3
4
// withCounter.js
function withCounter(WrappedComponent, incrementNum) {
//...程式碼
}
1
2
3
4
5
6
// HoverTitle.js
class HoverTitle extends React.Component {
//...程式碼
}

export default withCounter(HoverTitle, 5);

很多 library 都有用到 HOC,例:
React Redux 中的 connect。
react-router 中的 withRouter。
Material-UI 中的 withStyles。