0%

React Context

執行上下文(Execution Context)的概念(簡稱為 context)在 JavaScript 中非常重要。變數或函式的執行上下文定義了它可以存取運作的資料,以及它應該如何運作。 每個執行上下文都有一個關聯的 variable 物件,在這個執行上下文所定義的變數和函數,都存在這個 variable 物件上。此 variable 物件無法由程式碼作存取,一卻都在幕後運作。

在各種 JavaScript 運作的架構,都將此 context 概念運用在其結構上。GraphQL 有 context,React 也不例外。

在 React 中,context (上下文) 就像為數據設置噴射機一樣。 您可以通過創建 context provider (上下文提供者) 將數據放置在 React context 中。Context provider 是一個 React 組件,您可以將其包裹在整個組件樹或組件樹的特定部分中。

Context provider 是您的數據所在的出發機場。它也是航空公司的樞紐。所有航班都從該機場出發,飛往不同的目的地。每個目的地都是 context consumer (上下文使用者)。

context consumer 是一種從 context 檢索數據的 React 組件。這是您的數據著陸,下機並開始工作的目的地機場。

使用 context 將使我們能夠將數據存儲在單個位置,但是不需要我們將數據通過不需要它們的一堆組件傳遞。

將資料放置在 Context

為了在 React 中使用 context,我們必須首先在 context provider 中放置一些數據,然後將該 provider 添加到我們的組件樹中。React 有一個稱為 createContext 的函式,我們可以使用它創建一個新的 context 物件。 該物件包含兩個組件:context ProviderConsumer

讓我們將顏色數據 colors 放入 context 中。 通常都會將 context 添加到應用程序的入口點,然後就可以在每個組件中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="react-container"></div>

<script type="text/babel">
const { createContext } = React;
const { render } = ReactDOM;
const { uuid, colorData } = Demo;

const ColorContext = createContext();

...

render(
<ColorContext.Provider value={{ colors: colorData }}>
<App />
</ColorContext.Provider>,
document.getElementById("react-container")
);
</script>

第 8 行我們使用 createContext 創建了一個新的 React context 實例,我們將其命名為 ColorContext。color context 包含兩個組件:ColorContext.Provider 和 ColorContext.Consumer。

我們需要使用 provider 將 colors 數據置於狀態中。第 13 行通過設置 Provider 的 value 屬性,可以將數據添加到 context 中。 這裡,我們向 context 添加了一個包含 colorData 數據的 colors 屬性的物件。由於我們使用 provider 包裝了整個 App 組件,因此 colors 數據將可供我們整個組件樹中含有 context consumer 的組件使用。當組件想從 context 中獲取 colors 數據時,只需要訪問 ColorContext.Consumer。

Context provider 並不必總是需要包裝整個應用程序;可以只包裝特定部分的組件,這可以使您的應用程序有更好的效能。Provider 將僅向其子組件提供 context 值。因此可以在一個應用程序使用多個 context provider。

現在我們在 conext 中提供 colors 數據,App 組件不再需要保持狀態並將其作為 props 屬性傳遞給其子組件。我們可以直接飛過 App 組件。Provider 是 App 組件的父級,它在 context 中提供 colors。 ColorList 是 App 組件的子級,它可以直接自己獲取 colors。 因此 App 組件根本不需要接觸 colors,因為 App 組件本身與 colors 無關,這種保持狀態的責任已從組件樹中解脫了。

我們可以從 App 組件中刪除很多代碼。它只需要呈現 AddColorForm 和 ColorList。 它不再需要擔心數據狀態:

App
1
2
3
4
5
6
7
8
const App = () => {   
return (
<>
<AddColorForm />
<ColorList />
</>
);
};

使用 useContext 取得資料

React 16.8 之後添加的 Hooks 使處理 context 變得更簡易。現在使用 useContext 掛鉤,直接在掛鉤中訪問 Consumer,我們不再需要直接使用 Consumer 組件。在使用 Hooks 之前,我們必須使用 context consumer 中稱為 render props 的模式從 context 中獲取 colors。Render props 作為參數傳遞給子組件。 現在我們直接使用 useContext 掛鉤從 context consumer 中獲取我們需要的那些值。

ColorList 組件不再需要從其屬性獲取 colors。它可以通過 useContext 掛鉤直接訪問它們:

ColorList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { createContext, useContext } = React;

...

const ColorList = () => {
const { colors } = useContext(ColorContext);

if(!colors.length) return <div>No Colors Listed.</div>;

return (
<div>
{
colors.map(color => <Color key={color.id} {...color} />)
}
</div>
);
};

第 1 行將 useContext 加進來。第 6 行使用 useContext 掛鉤從 context consumer 中獲取我們需要的數據 colors。現在,ColorList 可以基於 context 中提供的數據構造用戶界面。

Stateful Context Provider

Context provider 可以將一個物件放入 context 中,但是不能自行更改 context 中的值。它需要父級組件的幫助。訣竅是創建一個能夠渲染 context provider 的有狀態父組件。當有狀態組件的狀態更改時,它將使用新的 context 數據重新渲染 context provider (context provider 也是一個 React 組件)。 而 context provider 的任何子代也將重新以新的 context 數據渲染介面。

這個會渲染 context provider 的有狀態組件是我們的 custom provider。 這個自定義的有狀態組件將我們的 App 組件與 provider 包裝在一起。

ColorProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { createContext, useContext, useState } = React;

...

const ColorProvider = ({ children }) => {
const [colors, setColors] = useState(colorData);
return (
<ColorContext.Provider value={{ colors, setColors }}>
{children}
</ColorContext.Provider>
)
};

render(
<ColorProvider>
<App />
</ColorProvider>,
document.getElementById("react-container")
);

在此 ColorProvider 組件中,我們使用 useState 掛鉤創建了 colors 狀態變數。 ColorProvider 使用 ColorContext.Provider 的 value 屬性將 colors 從狀態添加到 context。 ColorProvider 中的所有子組件都會因狀態的改變而重新渲染,而且也都包裝在 ColorContext.Provider 中,所以也都將可以從 context 直接訪問 colors 數據。

您可能已經注意到,我們在第 8 行也將 setColors 函式添加到 context 中。這使 context consumers 可以更改 colors 數據。每當調用 setColors 時,colors 數據都會更改。這將導致 ColorProvider 的渲染,我們的 UI 將被更新,以顯示新的 colors 數據。

將 setColors 添加到 context 可能不是最好的主意。它可能會讓其他開發人員和您在以後使用它時出錯。更改 colors 數據的值時只有三個選項:添加顏色,刪除顏色或對顏色進行評級。 最好在 context 中為每個操作各自添加函式功能。這樣,您就不必向 consumers 公開 setColors 函式。 您將只公開允許其進行更改的函式功能:

ColorProvider
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
const ColorProvider = ({ children }) => {
const [colors, setColors] = useState(colorData);

const addColor = (title, color) =>
setColors([
...colors,
{
id: uuid(),
rating: 0,
title,
color
}
]);

const rateColor = (id, rating) =>
setColors(colors.map(color => (color.id === id ? { ...color, rating } : color ))
);

const removeColor = id => setColors(colors.filter(color => color.id !== id ));

return (
<ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
{children}
</ColorContext.Provider>
)
};

看起來好多了。在此我們為 context 添加了可以在 colors 陣列上進行的所有操作函式,addColor、reteColor 與 removeColor。 現在,組件樹中的任何組件都可以使用這些函式更改 colors 數據。

Custom Hooks with Context

我們還可以做出一些改變。Hooks 的引入使得 context 完全不必暴露於 consumer 組件。 但對於團隊成員而言,context 可能會造成混亂。 通過將 context 包裝在自定義掛鉤(custom hooks)中,我們可以使它們更容易理解與維護。

無需公開 ColorContext 實例,我們可以創建一個名為 useColors 的 hook ,該 hook 會從 context 返回 colors 數據。

useColors
1
2
const ColorContext = createContext();
const useColors = () => useContext(ColorContext);

這一簡單更改對應用程序結構產生了巨大影響。我們可以將呈現和使用有狀態數據 colors 所需的所有功能包裝在一個自定義掛鉤中。

現在 consumer 使用 ColorProvider 和 useColors 掛鉤是一件比較愉快的事情。App 組件的所有子級組件都可以從 useColors 掛鉤中獲取 colors 數據。 ColorList 組件現在可以直接使用自行定義的 useColors 掛鉤取得 colors 數據:

ColorList
1
2
3
4
5
6
7
const ColorList = () => {
const { colors } = useColors();

...

return ( ... );
};

我們從該組件中刪除了對 context 的任何引用。 現在,我們需要的一切都可以從我們自定義的掛鉤中獲得。

Color 組件現在也可以使用我們自行定義的 useColors 掛鉤直接獲取用於對 color 進行評級和刪除顏色的函式 rateColor 與 removeColor:

Color
1
2
3
4
5
6
7
8
9
10
11
12
13
const Color = ({ id, title, color, rating }) => {
const { rateColor, removeColor } = useColors();
return (
<section>
<h1>{title}</h1>
<button className="t-Button" onClick={() => removeColor(id)} >
<FaRemove />
</button>
<div style={{ height: 50, backgroundColor: color}} />
<StarRating selectedStars={rating} onRate={rating => rateColor(id, rating)} />
</section>
);
};

最後則是 AddColorForm 組件直接從 useColors 掛鉤取得 addColor 函式:

AddColorForm
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
const AddColorForm = () => {
const [title, setTitle] = useState("");
const [color, setColor] = useState("#000000");
const { addColor } = useColors();

const submit = e => {
e.preventDefault();
addColor(title, color);
setTitle("");
setColor("#000000");
};

return (
<form onSubmit={submit} >
<input
value={title}
onChange={ event => setTitle(event.target.value)}
type="text"
placeholder="color title..."
required
/>
<input
value={color}
onChange={ event => setColor(event.target.value)}
type="color"
required
/>
<button className="t-Button t-Button--hot margin-left-sm">Add</button>
</form>
);
};

Custom Hooks

在 AddColorForm 組件中還有一個地方可以改進的。當您的大型表單具有很多 input 元素時,您可能會複製並貼上以下兩行代碼:

1
2
value={title}
onChange={ event => setTitle(event.target.value)}

通過簡單地將這些屬性複制並粘貼到每個表單元素中,同時調整變數名稱,似乎很快速。但是,腦海裡總覺得,複製和粘貼代碼表明這些應改可以用函式更抽象化這些冗餘的工作。

我們可以創建受控表單組件所需的詳細信息打包到自定義的掛鉤(Custom Hook)中。 我們可以創建自己的 useInput 掛鉤,可以抽像出創建受控表單輸入所涉及的冗餘部分:

useInput
1
2
3
4
5
6
7
const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return [
{ value, onChange: e => setValue(e.target.value) },
() => setValue(initialValue)
];
};

現在可以在 AddColorForm 使用自定義的 useInput 掛鉤:

AddColorForm
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
const AddColorForm = () => {
const [titleProps, resetTitle] = useInput("");
const [colorProps, resetColor] = useInput("#000000");
const { addColor } = useColors();

const submit = e => {
e.preventDefault();
addColor(titleProps.value, colorProps.value);
resetTitle();
resetColor();
};

return (
<form onSubmit={submit} >
<input
{...titleProps}
type="text"
placeholder="color title..."
required
/>
<input
{...colorProps}
type="color"
required
/>
<button className="t-Button t-Button--hot margin-left-sm">Add</button>
</form>
);
};
  • 第 2、3 行改用自定義的 useInput 掛鉤。
  • 第 16、22 行我們不用重複輸入 value 與 onChange 事件。
  • 記得要一起修改 submit 函式中第 8 行 addColor 的參數與第 9 、10 行預設值設定。這個預設值是我們在調用 useInput 時設定的。

不必靠組件屬性將數據傳上傳下的,整個程式碼更清楚與更容易理解。

Oracle APEX: Color Organizer
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<div id="react-container"></div>

<script type="text/babel">
const { createContext, useContext, useState } = React;
const { render } = ReactDOM;
const { uuid, colorData } = Demo;

const ColorContext = createContext();
const useColors = () => useContext(ColorContext);

const ColorProvider = ({ children }) => {
const [colors, setColors] = useState(colorData);

const addColor = (title, color) =>
setColors([
...colors,
{
id: uuid(),
rating: 0,
title,
color
}
]);

const rateColor = (id, rating) =>
setColors(colors.map(color => (color.id === id ? { ...color, rating } : color ))
);

const removeColor = id => setColors(colors.filter(color => color.id !== id ));

return (
<ColorContext.Provider value={{ colors, addColor, removeColor, rateColor }}>
{children}
</ColorContext.Provider>
)
};

const FaStar = ({ color, onSelect = f => f }) => (
<span className="fa fa-star" style={{color}} onClick={onSelect} ></span>
);

const Star = ({ selected = false, onSelect = f => f }) => (
<FaStar color={selected ? "gold" : "grey"} onSelect={onSelect} />
);

const createArray = length => [...Array(length)];

const StarRating = ({ totalStars = 5, selectedStars = 0, onRate = f => f }) => {
return (
<>
{createArray(totalStars).map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
onSelect={() => onRate(i + 1)}
/>
))}
<p style={{fontSize: "1.5em", margin: "10px"}}>{selectedStars} of {totalStars} stars</p>
</>
);
};

const FaRemove = () => <span className="fa fa-remove"></span>;

const Color = ({ id, title, color, rating }) => {
const { rateColor, removeColor } = useColors();
return (
<section>
<h1>{title}</h1>
<button className="t-Button" onClick={() => removeColor(id)} >
<FaRemove />
</button>
<div style={{ height: 50, backgroundColor: color}} />
<StarRating selectedStars={rating} onRate={rating => rateColor(id, rating)} />
</section>
);
};

const ColorList = () => {
const { colors } = useColors();

if(!colors.length) return <div>No Colors Listed.</div>;

return (
<div>{ colors.map(color => <Color key={color.id} {...color} />) } </div>
);
};

const useInput = initialValue => {
const [value, setValue] = useState(initialValue);
return [
{ value, onChange: e => setValue(e.target.value) },
() => setValue(initialValue)
];
};

const AddColorForm = () => {
const [titleProps, resetTitle] = useInput("");
const [colorProps, resetColor] = useInput("#000000");
const { addColor } = useColors();

const submit = e => {
e.preventDefault();
addColor(titleProps.value, colorProps.value);
resetTitle();
resetColor();
};

return (
<form onSubmit={submit} >
<input
{...titleProps}
type="text"
placeholder="color title..."
required
/>
<input
{...colorProps}
type="color"
required
/>
<button className="t-Button t-Button--hot margin-left-sm">Add</button>
</form>
);
};

const App = () => {
return (
<>
<AddColorForm />
<ColorList />
</>
);
};

render(
<ColorProvider>
<App />
</ColorProvider>,
document.getElementById("react-container")
);
</script>

Hook 是 React 16.8 中增加的新功能,它解決了 React 中我們過去在編寫與維護數萬個 component 組件時所遇到的各種看似不相關的問題。

Hook 可以讓你從 component 組件抽取 stateful 的邏輯,讓你不需要改變 component 階層就能重用 stateful 的邏輯,如此一來它就可以獨立地被測試和重複使用。