[React] 一文速通React
Adrian Chen

關於我為什麼要寫這篇文章,我有幾句話要講。本身作為一個前端開發人員,我一直有在用Vue.js Framework,鑑於其快速構建大型Web App的能力,我對其非常喜歡。然而近期我打算進軍Multi-Platform App的領域了,這樣其實作為一個Web開發人員來講,就沒有多少可以做的選擇了。Vue.js在跨裝置方面的應用尚且薄弱,而React家族的React Native和Google家的Flutter是這方面的絕對主力。

然而,“得益於”Flutter噁心的樣式互套機制,對開發人員極度不友好,我毫無猶豫地放棄了它。所以,React Native就成了我唯一的選擇。

所以讀者可以將這篇文章看作是React Native的前置知識。

本文的參考來自React的官方網站(https://react.dev/),感謝社區成員所作出的努力。

React 元件

React最重要的概念就是“元件”(Component),一個React元件就是一個返回UI組合的JSX函式。例如:

1
2
3
4
5
6
7
8
function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
>
);
}

可以將React元件抽離成一個檔案,即所謂的single-file component。例如我們可以在Profile.js檔案中匯出元件:

1
2
3
4
5
6
7
8
9
10
11
// Profile.js
function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
>
);
}

export default Profile

然後我們就可以在Main.js元件中使用這個元件:

1
2
3
4
5
6
import Profile from './Profile.js'
function Main() {
return (
<Profile></Profile>
);
}

React元件在使用的時候需以大寫字母開頭,以區別普通的DOM元素。例如<Profile></Profile>

JSX

React使用的是JS的語法擴充套件JSX,這個套件可以允許在JS檔案中書寫HTML類似的標記。

JSX有一套嚴格的規則,不同於HTML,使用時要特別注意:

  • 每一個React元件的返回值必須是單根的。即所有元素必須被包含在一個根元素中。React推薦使用<></>
1
2
3
4
5
6
7
8
function SingleRoot() {
return (
<>
<div>Hello, world.</div>
<img src="https://i.imgur.com/MK3eW3As.jpg" >
</>
);
}
  • 所有的標籤必須關閉:使用自閉標籤例如<img/>或關閉的標籤<li></li>。這一點對於有良好開發習慣的開發人員來講不成問題。

使用大括弧{}來開啟JavaScript程式碼窗

類似於Vue.js中的JavaScript Template Reflect機制,在JSX中,我們也可以在返回的JSX DOM Tree中嵌入我們的JS程式碼。這時候我們需要使用{}來開啟JS程式碼窗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function Avatar() {
const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
const description = 'Gregorio Y. Zara';
return (
<>
<img
className="avatar"
src={avatar}
alt={description}
/>
<div>{description}</div>
</>
);
}

JS程式碼窗可以用在標籤之間的HTML Text上,也可以用在標籤的屬性取值上,就如同上面的例子展示的一樣。

我們還可以在大括弧中傳遞JS物件:

1
2
3
4
5
6
7
8
9
10
11
12
export default function TodoList() {
return (
<ul style={{
backgroundColor: 'black',
color: 'pink'
}}>
<li>Improve the videophone</li>
<li>Prepare aeronautics lectures</li>
<li>Work on the alcohol-fuelled engine</li>
</ul>
);
}

我們分析一下<ul></ul>這個標籤。其style屬性中,第一個大括弧表示開啟JS程式碼窗,第二個大括弧則是JS物件的大括弧

透過向標籤屬性中傳遞JS物件,我們可以實現CSS的動態傳遞。

向元件中傳遞props

Vue.js中也有props傳遞的概念,JSX類比與那個概念。由於React元件屬於JS函式,因此,props可以作為函式的引數來傳遞,這一點確實比Vue.js要來得方便。

1
2
3
4
5
6
7
8
9
10
// Profile.js
// 使用{}來定義props
export default function Profile({avatar, name}) {
return (
<>
<img src={avatar}/>
<div>{name}</div>
</>
);
}

然後在main.js中:

1
2
3
4
5
6
7
8
9
10
11
12
// main.js
import Profile from './Profile.js'
function Main() {
const avatarUrl = "https://i.imgur.com/7vQD0fPs.jpg"
const name = "Gregorio Y. Zara"
return (
<Profile
avatar={avatarUrl}
name={name}
></Profile>
);
}

還可以為props設定預設值:

1
2
3
function profile({avatar, name = "Gregorio Y. Zara"}) {
...
}

props透傳

如果一個元件希望將自己的所有props傳遞給子元件,那麼我們有一個props透傳的簡潔語法:

1
2
3
4
5
6
7
function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}

這將把Profile元件的所有props全部透傳到Avatar元件。

props是不可改變的。當我們要props改變的時候,我們其實是需要傳遞一個新的props,而不是改變舊的。這一點將會在下面講到。

條件渲染

類似於Vue.js中的v-ifv-else。由於JSX使用函式渲染的模式,因此我們可以很方便地使用JS的if-else來進行條件渲染。

1
2
3
4
5
6
function Item({ name, isPacked }) {
if (isPacked) {
return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;
}

可以使用null來什麼也渲染空:

1
2
3
4
if (isPacked) {
return null;
}
return <li className="item">{name}</li>;

列表渲染

類似於Vue.js中的v-for。官方提供的步驟如下:

  1. 創建陣列:
1
2
3
4
5
6
7
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. 把陣列map到JSX node中:
1
const listItems = people.map(person => <li>{person}</li>);
  1. 在元件中返回map的結果:
1
return <ul>{listItems}</ul>;

可以使用key屬性在陣列中唯一標示該元素,類似於Vue.js中的:key

1
return <li key={person.id}>...</li>

這個key應該是獨一無二的。

事件處理

透過在元件中定義事件處理函式的方法可以實現事件處理:

1
2
3
4
5
6
7
8
9
10
11
export default function Button() {
function handleClick() {
alert('You clicked me!');
}

return (
<button onClick={handleClick}>
Click me
</button>
);
}

事件處理函式有幾個注意事項:

  1. 必須定義在元件中。
  2. 名稱通常使用handle開頭,後接事件名稱。

自然,你也可以直接將函式定義在JSX node中:

1
2
3
<button onClick={function handleClick() {
alert('You clicked me!');
}}>

由於事件處理函式是定義在元件內部的,因此該函式也可以直接使用元件的props。

客製化事件

在Vue.js中,我們可以很方便地客製化事件。使用defineEmits()函式配合emit()函式,我們可以向父元件丟出一個事件。

在React中,這個過程大差不差。defineEmits()中的定義被轉成了函式類型的props,而後直接執行之即可。

1
2
3
4
5
6
7
8
9
10
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
onClick();
}}>
{children}
</button>
);
}

事件傳播

我們有這樣一個元件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<button onClick={() => alert('Playing!')}>
Play Movie
</button>
<button onClick={() => alert('Uploading!')}>
Upload Image
</button>
</div>
);
}

我們發現,當我們點按任意一個Button,不僅會觸發Button本身的onClick()事件,也觸發了其父div的onClick()

要停止事件向其父傳播,我們需要呼叫其預設引數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<button onClick={(e) => {
// 呼叫e.stopPropagation()函式停止事件傳播

e.stopPropagation()
alert('Playing!')
}}>
Play Movie
</button>
<button onClick={() => alert('Uploading!')}>
Upload Image
</button>
</div>
);
}

與之類似,e.preventDefault()函式可以避免預設的元素行為。

狀態管理

由於互動的結果,元件通常需要更改螢幕上的內容。在表單中鍵入應更新輸入欄位,單擊影象列上的“Next”應更改顯示的影象,單擊“購買”應將產品放入購物車。元件需要“記住”事情:當前輸入值、當前影象、購物車。在React中,這種特定於元件的記憶體被稱為狀態(state)。

在Vue.js中,也有類似的概念。例如,當我們需要將某個變數動態渲染到template上的時候,我們就需要使用ref()reactive(),否則,對變數的任何修改都不會被渲染器意識到。

比如在下面的程式中,點按Change Index按鈕就不會發生任何渲染改變:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function Test() {
let index = 0

function handleClick() {
index = index + 1
}
return (
<>
<div>{index}</div>
<button onClick={handleClick}>Change Index</button>
</>
);
}

在React中也是如此,不同的是,在React中,我們使用的是useState()函式。它的使用過程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useState } from 'react';

export default function Test() {
const [index, setIndex] = useState(0)

function handleClick() {
setIndex(index + 1)
}
return (
<>
<div>{index}</div>
<button onClick={handleClick}>Change Index</button>
</>
);
}

我們可以看到,原本的變數被變成了一個變數陣列,其中index是原來的變數,而配套的setIndex則是變數的setter。

在React中,useState以及以“use”開頭的任何其他函式都被稱為Hook。Hook是特殊函式,僅在React渲染時可用。

與Vue.js類似,React中狀態也是元件依賴的。也就是說,如果你把一個元件渲染兩次,這兩個元件將會有完全不互通的各自狀態。即狀態是“私有的”。

狀態批次處理

我們來看這樣一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}

你可能會期望,當我點按“+3”按鈕的時候,number能夠連續相加3次,畢竟我使用了三次setNumber()函式。

然而你會發現,每點按一次,number僅+1。這是因為,在每一輪渲染之前,number的值都是固定的,因此無論在本輪渲染中呼叫了多少次setNumber()函式,其都是一樣的效果。

React的狀態批次處理機制是:每一輪渲染之前,都要將所有的狀態更新乃至於所有的事件處理函式全部執行完成,才會開始這一輪渲染。所以會出現上面的那個問題。

儘管像上面那樣子寫程式不是一個好主意,上面的需求也算是相當小眾,然而React仍舊給我們提供了解決的方案,只需要將setNumber(number + 1)替換成setNumber(n => n + 1)即可。這告訴React,用上一輪更新的狀態結果來做事,而不是使用預設固定值。

這裡的n => n + 1函式稱為“更新函式”,通常情況下,使用被更新變數的第一個字母作為該函式的引數。

物件狀態的更新

遵循一個原則:不要修改已經持有的物件(儘管它們是mutable的),而是創造一個新的物件去替換原有的物件。

1
2
3
4
setPosition({
x: e.clientX,
y: e.clientY
});

以上的更新適用於物件中的每一個鍵的值都發生變化的情況。如果物件中只有少數的鍵發生變化,每一次都要更新整個物件,這顯然太不合理了。解決這個問題,我們可以使用...來拷貝原來的物件變數值。

1
2
3
4
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});

好,問題來了,假如我們有一個複雜的Nested物件,我們應該如何去更新它?比如這個物件:

1
2
3
4
5
6
7
8
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

如果我想要更新artwork中的city一欄,按照上面說的,我們自然可以這樣子去更新它:

1
2
3
4
5
6
7
setPerson({
...person,
artwork: {
...person.artwork,
city: 'New Delhi'
}
});

我們不得不使用了兩遍...。這個還算好,如果遇到更加複雜的nested情況,這樣做顯然來得效率低。

隆重介紹我們寫React Native第一個必備的外部套件——use-immer

使用use-immer套件,我們可以反直覺地無痛更新物件。

首先我們安裝use-immer套件:

1
npm install use-immer --save

然後我們就可以把useState()換成useImmer()了:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { useImmer } from 'use-immer';

export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}

function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}

function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}

function handleImageChange(e) {
updatePerson(draft => {
draft.artwork.image = e.target.value;
});
}

return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}

其中的更新語句:

1
2
3
updatePerson(draft => {
draft.artwork.title = e.target.value;
});

允許我們直接更新物件中的鍵。這很酷,對吧?

陣列狀態的更新

陣列可以被看作特殊的物件,因此對於物件的限制也同樣適用於陣列。我們也依然需要將陣列看作immutable,儘管其依然是mutable的。

對於陣列,我們可以使用一些更加簡單的方式去生成新的陣列。

向陣列中新增元素

push()方法是mutable方法,要實現這個內容,需要:

1
2
3
4
setArtists([
{ id: nextId++, name: name },
...artists
]);

刪除陣列中的元素

可以曲線救國,使用filter()函式篩選出非刪除元素組成新陣列。

1
2
3
4
5
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);

修改陣列

使用map()函式來客製化修改規則,生成新的陣列。

1
2
3
4
5
6
7
8
9
10
11
12
setArtists(shapes.map(shape => {
if (shape.type === 'square') {
// No change
return shape;
} else {
// Return a new circle 50px below
return {
...shape,
y: shape.y + 50,
};
}
}))

陣列中插入元素

配合slice()函式來使用:

1
2
3
4
5
setArticles([
...artists.slice(0, insertAt),
{ id: nextId++, name: name },
...artists.slice(insertAt)
])

其他修改

其實,對於陣列的修改還有一個最簡單不過的方式:直接拷貝原來的陣列到一個新陣列,然後修改新陣列即可。

1
2
3
const newArticles = [...Articles]
// Here are some change to the newArticle veriable.
setArticles(newArticles)

然而這樣做對於陣列中的物件來說是一個問題。因為這樣的拷貝很淺,無法將物件的邏輯拷貝到新陣列。因為陣列中的物件實際上是一個指向,而不是一個元素。所以當我們修改新陣列中的物件內容,事實上是在修改原陣列,這是應當被避免的。

可以這樣做:

1
2
3
4
5
6
7
8
9
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));

或者你會更喜歡使用immer:

1
2
3
4
5
6
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});

狀態管理的高級玩法

對於React狀態管理和宣告式程式設計的思考

初學者對於React的狀態管理可能會感到非常混亂和迷惑,但是如果習慣起來,會發現React的狀態管理功能十分強大。

按照React官方的說法,其狀態管理屬於“宣告式程式設計”的典型應用。

說到宣告式,我上次接觸的以它為特點的東西還叫做NixOS,那個Linux發行版給我留下了極其深刻的心理陰影。雖然我承認其一些設計理念確實是超前的且無可比擬的,比如說宣告式設計。然而由於其軟體的缺失和pack的過程之複雜,實在是讓我記憶深刻,並且終生不想再碰它。

跑遠了~回到宣告式程式設計這件事上來。什麼叫宣告式呢?React官方給了一個十分形象的例子:

假如你叫了一輛計程車,想要去到某個地方,使用傳統的命令式程式設計,你就需要一步一步指導司機走哪條路,最後到達你想要到達的地方。如果其中一步錯誤,可能最後就無法到達正確的目的地。

但是宣告式程式設計則不一樣。其相當於你直接告訴司機你要去到哪個地方,然後司機會幫你完成路線規劃,最後去到那裏。

相當於,你只需要宣告結果,過程不需要去管。這在一些大型的程式系統中會顯著提高開發和維護的效率。

回到React上來,你只需要宣告頁面的“狀態”,React就能夠幫助你達到那個狀態,而不需要你親自去操縱頁面上的元件之類的。

事實上,Vue.js所基於的template系統也是一樣。template預先聲明了頁面的元素,然後我們可以修改template的狀態來實現頁面的更新。

在元件之間共享狀態

有的時候我們希望兩個元件的狀態可以同步改變,這時候我們需要進行“提升狀態”作業。

提升狀態,是指將狀態從元件中刪除掉,轉而將其放到共同的最近父元件中的過程。

我們來看這樣一個例子:

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
30
31
import { useState } from 'react';

function Panel({ title, children }) {
const [isActive, setIsActive] = useState(false);
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={() => setIsActive(true)}>
Show
</button>
)}
</section>
);
}

export default function Accordion() {
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About">
Children1
</Panel>
<Panel title="Etymology">
Children2
</Panel>
</>
);
}

顯然,這兩個Panel的isActive狀態是獨立的。

如果我希望讓它們兩個能夠同時改變,即要展示就共同展示,要隱藏就共同隱藏。這該如何做呢?

  1. 從子元件中刪除狀態。
  2. 從父元件中向子元件傳遞硬編碼資料。
  3. 將狀態給到父元件。

例如上述例子就可以如下修改:

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
30
31
32
33
import { useState } from 'react';

function Panel({ title, children, isActive, onShow }) { // 2. isActive, onShow:父元件向子元件傳遞硬編碼資料
// 1. 刪除子元件的狀態
return (
<section className="panel">
<h3>{title}</h3>
{isActive ? (
<p>{children}</p>
) : (
<button onClick={onShow}>
Show
</button>
)}
</section>
);
}

export default function Accordion() {
// 3. 把狀態轉移到父元件
const [isActive, setIsActive] = useState(false)
return (
<>
<h2>Almaty, Kazakhstan</h2>
<Panel title="About" isActive={isActive} onShow={() => setIsActive(!isActive)}>
Children1
</Panel>
<Panel title="Etymology" isActive={isActive} onShow={() => setIsActive(!isActive)}>
Children2
</Panel>
</>
);
}

提升狀態的過程本質上是父元件統一管理子元件的過程。

保留和重置狀態

只要元件在UI tree的相同地方被渲染,React就會保留其狀態,而當其被移除,或者其他元件代替了它在UI tree中的位置,它的狀態就會被丟棄。

相同位置的相同類型元件保留狀態

有如下一個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function App() {
const [show, setShow] = useState(true)
return (
<div className="Root">
{ show ? (<Test>Children1</Test>) : (<Test>Children2</Test>)}
</div>

<button onClick={() => setShow(!show)}>Click me.</button>
);
}

function Test({children}) {
const [score, setScore] = useState(0)
return (
<div>score<div/>
<div>{children}</div>
<button onClick={() => setScore(score + 1)}>Add</button>
);
}

你會發現當你按下Click me.按鈕的時候,即使Children1變成了Children2,元件裡面的狀態score也不會改變。這是因為兩個Test元件是UI tree相同位置的相同類型元件,因此會被React當作相同元件,狀態得到保留,

同一位置的不同類型元件重置狀態

根據上面的例子,我們應該十分容易理解它,比如同一位置,把Test元件換成其他的什麼元件,再換回來,Test元件的score狀態自然就被重置了。

在同一位置重置狀態

上面我們講到,元件遵循的是在同一位置的相同類型元件將會保留狀態。然而如果我們希望在同一位置的相同元件強制重置狀態,我們該如何做呢?

我們有兩種方法:

  1. 在不同的位置渲染。
  2. 使用key。

我們來看第一種。還是用上面的那個例子。將:

1
{ show ? (<Test>Children1</Test>) : (<Test>Children2</Test>)}

修改成:

1
2
{ show && <Test>Children1</Test> }
{ !show && <Test>Children2</Test> }

即可。

另一種方式是使用key。key可以讓React區分不同的元件。只需要給到兩個<Test></Test>元件兩個不同的key即可。

1
{ show ? (<Test key="Test1">Children1</Test>) : (<Test key="Test2">Children2</Test>)}

使用Reducer

當我們的狀態處理邏輯越來越多的時候,難免會顯得非常混亂。這個時候,我們可以將狀態處理邏輯統一提取到元件外面的reducer函式中,將針對某一個狀態變數的全部處理邏輯都放到這裡面。然後在元件內部,只需要使用這個reducer函式來分發相應的處理邏輯就好。

首先我們需要一個reducer函式,它接受兩個引數:state和action,state是要監控的狀態變數,action是執行的作業。該函式回傳更新後的狀態。

比如我們編寫這樣一個函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}

在元件中使用reducer:

1
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer()函式接受兩個引數:taskReducer是在元件外定義的reducer函式,initialTasks是預設的狀態值。

當需要更新元件的狀態時,我們只需要使用dispatch()函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}

傳送給dispatch()函式的引數是一個物件,這個物件最後會變成taskReducer()函式的第二個引數action

我們還可以使用immer來提升我們使用Reducer的體驗。(immer真是個偉大的發明)

使用immer,我們就可以使用pusharray[index] =這種形式來修改我們的state。

只需要將useReducer()換成userImmerReducer()即可,然後你就可以在你的reducer函式中大膽使用上面提到的修改方法了。

使用context深入傳遞資料

這個場景在客製化元件庫的時候特別有用。在之前使用Vue.js開發元件庫的時候,我總會遇到這樣一個問題:客製化的radio-box元件需要監聽其父級radio-box-group元件的屬性,來確定自己是不是已經被選中了。

context就可以做到這一點。通俗講,context可以讓父級(不管多遙遠)的元件可以跨越千山萬水,為子級元件提供一些資料。

我們就用這個例子來做。現在我有一個RadioBoxGroup元件,其有一個屬性是check,類型是integer,代表選中的RadioBox元件的id。其children為RadioBox元件。要求RadioBox元件能夠根據其父級的check,自動獲悉自己是否被選中。

我們首先建立這兩個元件,我分為兩個檔案:

1
2
3
4
5
6
7
8
9
// RadioBox.js
export default function RadioBox({id, children}) {
return (
<>
<input type="radio">
<span>{children}</span>
</>
)
}
1
2
3
4
5
6
7
8
// RadioBoxGroup.js
export default function RadioBoxGroup({children}) {
return (
<>
{children}
</>
);
}

然後再建立一個測試元件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// App.js
import RadioBox from './RadioBox.js';
import RadioBoxGroup from './RadioBoxGroup.js';
export default function App() {
return (
<>
<RadioBoxGroup>
<RadioBox id="1"></RadioBox>
<RadioBox id="2"></RadioBox>
<RadioBox id="0">保密</RadioBox>
</RadioBoxGroup>
</>
);
}

接下來我們就要開始實現上述目的了。

首先我們需要創建一個context,我們重新建立一個新的檔案用於專門存放context:

1
2
3
4
// CheckContext.js
import { createContext } from 'react';

export const CheckContext = createContext("0");

createContext()函式唯一的引數,表示預設的context值。

然後,我們在RadioBoxGroup元件中提供這個context:

1
2
3
4
5
6
7
8
9
10
// RadioBoxGroup.js
export default function RadioBoxGroup({check, children}) {
return (
<>
<CheckContext.provider value={check}>
{children}
</CheckContext.provider>
</>
);
}

這一步是告訴React:如果我的子元件索要context,請將這個context提供給它。React將會把距離子元件最近的context提供給它。

而後,子元件就可以向父元件索要context了:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useContext } from 'react';
import { CheckContext } from './CheckContext.js';

export default function RadioBox({id, children}) {
// 索要CheckContext
const check = useContext(CheckContext)
return (
<>
<input type="radio" checked={check === id}>
<span>{children}</span>
</>
)
}

完成了。接下來,就只要在App元件中為RadioBoxGroup元件設定check屬性即可。

結語

暫時先寫這麼多吧,也很多了。應當注意的是,這些內容僅僅在React官方檔案中屬於“中級”,而更高級的功能我暫時用不上,於是就暫時先寫到這裡。以後用到了,以後再說。

另外要再次提醒讀者,React是一個函式庫,而並非是一個框架。基於React的框架,請移步目前最流行的Next.js