# React 筆記 (react-router, context, reducer)
此筆記是基於 zerotomastery.io 推出 React 課程 [Complete React Developer in 2022](https://www.udemy.com/course/complete-react-developer-zero-to-mastery/) 所撰寫。
# 第 2 節: React Key Concepts
本節重點:
1. Don’t touch the DOM.
2. Build websites like lego blocks.
3. Unidirectional data flow ⇒ 容易 debug,源頭的 state 容易除錯
4. Cross platfoms:
- React Native (mobile apps)
- React 360 (VR apps)
- React Desktop
- React blessed (teminal)
## 7. Declarative ****宣告式**** vs Imperative ****命令式****
- Imperative paradigm:
You directly change indivisual parts of your app in response to various user events.
- Declarative paradigm:
You declare a state and REACT automatically does it for us.
## 8. Component Architecture 元件結構
- React is designed around the concept of reusable components.
## 11. How To Be A Great React Developer
### The job of a REACT developer
1. Decide on Components.
2. Decide the State and where it lives.
3. What changes when state changes.
# 第 3 節: React Basics
## 20. Create React App - NPX
### 什麼是 NPX?
NPM 版本大於等於 5.2 版就會自動附加 NPX。
**NPX 指定用來安裝並且安裝完後會執行,執行完後會刪除,所以不會佔容量。**
## 22. Create React App - React-Scripts 2
```json
// npx create-react-app project 之後的 package.json
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
}
```
- `react-scripts build` or `yarn build` 會打包好寫好的 App.js 並在根目錄生成一個 build 資料夾
- **BABEL** 會將程式碼轉換成很基礎的 JS code 讓不同家瀏覽器都看懂。
- **Webpack** 會將程式碼進行模組化,大檔案拆分成 `.chunk.js`,提升效能。
- `react-scripts eject` 99.9% 的機率不會用到,這用來更改打包的方式。
## 27. Monsters Rolodex - Class Components 類別型元件寫法
```jsx
// 1. import Component
import { Component } from 'react';
// 2. 宣告 class extends Component
class App extends Component {
// 3. 新增 render(),裡面 return JSX
render(){
return (
<div className="App">
<header className="App-header">
</header>
</div>
);
}
}
export default App;
```
## 28. Monsters Rolodex - Component State
```jsx
import { Component } from 'react'
class App extends Component {
// 1. 要先有 constructor 方法
constructor() {
// 2. 一定要先呼叫 super();super() 用來呼叫父類別的 constructor
super()
// 3. 才可以用 this
this.state = {
name: 'Robin',
}
}
render() {
return (
<div className="App">
<header className="App-header">
<p>{this.state.name}</p>
<button>change name</button>
</header>
</div>
)
}
}
export default App
```
## 29. Monsters Rolodex - setState & 30. Monsters Rolodex - States and Shallow Merge
**本兩小節重點:**
- React 只有在 state 物件的記憶體位址改變時才會重新更新元件。
- `this.setState()` 會創造一個新的物件並且用 shallow merge 的方式去更新值,如本專案 state 有 name、company 兩種屬性,在 setState 的時候只有更新 name 的值 (`this.setState({ name: 'Kate' })`),state 會變成 `{name: ‘Kate’, company: ‘Google’}`,**沒有被更新到的屬性還是會被保留!**
承上上述範例,若在按鈕上新增點擊事件,點擊後將名字從 Robin 改為 Kate,直接寫 `this.state.name = 'Kate';` 是不行的,儘管值確實被改變了,但是 state 物件的記憶體位址是一樣的,所以 React 不會重新渲染畫面。
必須使用 `this.setState` 的方式並傳入屬性值去覆蓋原本的屬性:
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
name: 'Robin',
company: 'Google'
};
}
render() {
return (
<div className="App">
<header className="App-header">
<p>{this.state.name}</p>
<button
onClick={() => {
// this.state.name = 'Kate';
this.setState({ name: 'Kate' })
}}
>
change name
</button>
</header>
</div>
);
}
}
export default App;
```
## 31. Monsters Rolodex - setState and Secondary Callback
**本小節重點:**
`this.setState()` 的參數也可接受一個 callback 函式,並且還可以再接收一個 optional 的 callback 函式,第一個 callback 用來 shallow merge `this.state` 的值,第二個 callback 會在 state 值更新完後執行(可處理 setState 是非同步的問題)
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
name: 'Robin',
company: 'Google',
};
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>{this.state.name}</p>
<button
onClick={() => {
this.setState(
() => {
return { name: 'Kate' };
},
// 第二個 callback 為 optional 可不加
() => {
console.log(this.state.name);
}
);
}}
>
change name
</button>
</header>
</div>
);
}
}
export default App;
```
## 36. Monsters Rolodex - Lifecycle Method: componentDidMount 實作開始 ⇒ 拿資料
- `componentDidMount`:component 初次被放入 DOM 的時候,一個元件的生命週期中只會發生一次。
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
// 1. 已知 monsters 要拿來 map(),初始值給空陣列
this.state = {
monsters: [],
};
}
// 2. 生命週期拿資料
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => {
// 3. 拿到資料後 setState 回去
this.setState(
() => {
return { monsters: users };
},
() => {
console.log(this.state);
}
);
});
}
render() {
return (
<div className="App">
// 略...
</div>
);
}
}
export default App;
```
## 38. Monsters Rolodex - Renders & Re-renders in React (類別型元件渲染順序)
**本小節重點:**
以下程式碼 console.log 的執行順序為 ⇒ 1 2 3 2
1. `cosntructor()` 會先執行,初始化 state 的值。
2. 再來執行 `render()`。
3. 接著 `componentDidMount()` 這裡拿取資料並更新 state 的值,一旦呼叫 `setState()` 就會再 `render()` 一次,所以又印出一個 2。
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
monsters: [],
};
console.log(1);
}
componentDidMount() {
console.log(3);
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => {
this.setState(
() => {
return { monsters: users };
},
() => {
// console.log(this.state);
}
);
});
}
render() {
console.log(2);
return (
<div className="App">
// 略...
</div>
);
}
}
export default App;
```
## 類別型元件生命週期補充
> 下列圖片引用自張至寧老師


## 40. Monsters Rolodex - Searching & Filtering(searchbox 字串篩選)
**本小節重點:**
1. `<input>` 掛 onChange 事件取得 `e.target.value`,用這個值去 filter array。
2. filtered array 用 setState 去更新 state 中的 array 的狀態。
3. 這邊使用 `String.includes()` 去比對字串,要注意的是 `.includes()` 會判別大小寫,所以此項目會一律先 `toLowerCase()` 再去篩選。
- 程式碼:
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
monsters: [],
};
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => {
// console.log(users);
this.setState(
() => {
return { monsters: users };
}
// ,
// () => {
// console.log(this.state);
// }
);
});
}
render() {
return (
<div className="App">
<input
type="search"
className="search-box"
placeholder="monster's name"
onChange={(e) => {
// console.log(e.target.value);
const inputString = e.target.value.toLowerCase();
const filteredMonsters = this.state.monsters.filter((monster) => {
return monster.name.toLowerCase().includes(inputString);
});
this.setState(() => {
return { monsters: filteredMonsters };
});
}}
/>
{this.state.monsters.map((monster) => {
return <h1 key={monster.id}>{monster.name}</h1>;
})}
</div>
);
}
}
export default App;
```
## 42. Monsters Rolodex - Storing Original Data (searchbox 字串篩選優化)
**本小節重點:**
本來做法是在 onChange 事件中直接 setState array 的值,但正確的情況是每次進行篩選時,array 都要初始值,所以這邊寫法改成 setState searchString,不要去 setState array。
- 程式碼:
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
monsters: [],
searchString: '',
};
}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => {
// console.log(users);
this.setState(
() => {
return { monsters: users };
}
// ,
// () => {
// console.log(this.state);
// }
);
});
}
render() {
const filteredMonsters = this.state.monsters.filter((monster) => {
return monster.name.toLowerCase().includes(this.state.searchString);
});
return (
<div className="App">
<input
type="search"
className="search-box"
placeholder="monster's name"
onChange={(e) => {
this.setState(() => {
const searchString = e.target.value.toLowerCase();
return { searchString };
});
}}
/>
{filteredMonsters.map((monster) => {
return <h1 key={monster.id}>{monster.name}</h1>;
})}
</div>
);
}
}
export default App;
```
## 43. Monsters Rolodex - Optimizations(效能優化)
**本小節重點:**
- 事件 Handler 不要寫在 `render()` 裡面,應該要盡可能在 class 內宣告成一個方法。
在本次專案範例中原本是將 handler 以函式表達式的形式寫在 JSX 中,如 `onChange={(e)⇒{…}}`,但這樣做在每次事件觸發時都會重新建造這個 handler。在 class 內宣告成一個方法,只需要建造一次 handler,來達到效能提升。
- 解構 ⇒ 將 `this.state.monsters` 和 `this.onSearchChange` 用解構提取變數出來,程式碼看起來較整潔。
- 程式碼:
```jsx
import { Component } from 'react';
class App extends Component {
constructor() {
super();
this.state = {
monsters: [],
searchString: '',
};
}
onSearchChange = (e) => {
const searchString = e.target.value.toLowerCase();
this.setState(() => {
return { searchString };
});
};
componentDidMount() {
// 略...
}
render() {
const { monsters, searchString } = this.state;
const { onSearchChange } = this;
const filteredMonsters = monsters.filter((monster) => {
return monster.name.toLowerCase().includes(searchString);
});
return (
<div className="App">
<input
type="search"
className="search-box"
placeholder="monster's name"
onChange={onSearchChange}
/>
{filteredMonsters.map((monster) => {
return <h1 key={monster.id}>{monster.name}</h1>;
})}
</div>
);
}
}
export default App;
```
### 補充(很重要)
上述範例中 class App 的 onSearchChange 方法是以箭頭函式作宣告,要注意的是這邊的 `this.setState(…)` 中的 `this`,是來自於外部環境,因為箭頭函式沒有自己的 `this`:
```jsx
onSearchChange = (e) => {
const searchString = e.target.value.toLowerCase();
this.setState(() => {
return { searchString };
});
};
```
若用非箭頭函式來宣告此 handler 的話,運行這個專案會拋出 **Uncaught TypeError: Cannot read properties of undefined (reading 'setState')。**因為實際上這邊的 `this` 會指向**全域物件**:
```jsx
onSearchChange(e) {
const searchString = e.target.value.toLowerCase();
// this 為 undefined,請看以下說明
this.setState(() => {
return { searchString };
});
}
```
以下內容節錄並改寫自 [React 與 bind this 的一些心得](https://medium.com/reactmaker/react-%E8%88%87-bind-this-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%BF%83%E5%BE%97-323c8d3d395d):
> 當使用 `extend React.Component` 的方式去宣告元件的時候,React 確實會綁定 `this` 到元件內,但是卻有以下特定的地方才會被綁進去
>
> 1. 生命周期函式,例如:`constructor()`、 `componentDidMount()` 等等
> 2. `render()` 內
>
> 其他自己定義的 property 就不會被綁入 `this` ,而且 `this` 會被指到 `windows` 這個全域上。
>
但又因為在嚴謹模式下,this 指向全域物件這件事是無效的,所以本範例中的 `this` 為 `undefined`!
**解決方法:**
我們可以在 `constructor()` 中利用 `bind()` 作 self-binding 就可將 this 重新指向,不會再報錯:
```jsx
constructor() {
super();
this.state = {
monsters: [],
searchString: '',
};
this.onSearchChange = this.onSearchChange.bind(this);
}
```
- 參考資料:
- **[In handleIncrement method, "this" is referring to undefined](https://stackoverflow.com/questions/69035073/in-handleincrement-method-this-is-referring-to-undefined)**
- **[why this resolves to undefined not window/global-env in react component method if it not bind this in constructor](https://stackoverflow.com/questions/55989840/why-this-resolves-to-undefined-not-window-global-env-in-react-component-method-i)**
- [**Dealing with Undefined 'this' in React Event Handlers in a Performant Way**](https://www.voitanos.io/blog/deal-with-undefined-this-react-event-handler-performant-way/)
## 45. Monsters Rolodex - CardList Component
**本小節重點:**
1. 將 CardList 分解成一個 component。
2. 講師建議元件名稱可取為 `xxx.component.jsx`。
## 46. Monsters Rolodex - Component Props
**本小節重點:**
在 `render(){}` 中取用並解構 `this.props`。
## 47. Monsters Rolodex - Rendering and Re-rendering part 2 類別型元件重新渲染(重要)
**本小節結論:**
在兩種況下元件會重新渲染 (re-render):
1. `this.setState()` 被呼叫時 ⇒ this.state 更新
2. `this.props` 的值更新時
## 48. Monsters Rolodex - SearchBox Component
**本小節重點:**
將 SearchBox 分解成一個 component。
## 49. Monsters Rolodex - CSS in React
在 components 底下的子資料夾中直接新增該元件的 CSS 檔,再 import 到元件裡面

**要注意的是這寫的寫法 CSS 會在整個 App.js 生效,CSS 有可能發生覆寫的情況。**
## 55. Monsters Rolodex - Functional Component Intro 函式型元件
- 函式型元件沒有生命週期
## 56. Pure & Impure Functions 純粹函式
- 以下資料節錄自 **[什麼是 Pure Function?在 React 當中的重要性是什麼?](https://dev.to/simonecheng/shi-mo-shi-pure-functionzai-react-dang-zhong-de-zhong-yao-xing-shi-shi-mo--1kjg)**
> 簡單來說,一個 function 只要符合以下兩個條件:
>
> 1. 相同的 input,永遠都輸出相同的 output。
> 2. 沒有產生 side effect。跟其他function不會互相干擾,不會修改/引用/存取或是依賴到到外部變數,但是當作參數傳入是可以的。
> 雖然 side effect 聽起來很像是負面的名詞,但不表示 side effect 就是不好的,在程式當中,side effect 單純就只是描述在寫 function 時有可能會出現的情況或是現象而已。
>
>
> **side effect 有哪些?**
>
> 以下介紹一些常見的 side effect,但不限於此:
>
> 1. Making a HTTP request
> 2. Mutating data
> 3. Printing to a screen or console
> 4. DOM Query/Manipulation
> 5. Math.random()
> 6. Getting the current time
**本小節結論:**
- **Hooks creates impure functions.**
- Generally speaking, we want to write functions purely, But when you write functional components, you will create impure functions.
**補充**
以下內容節錄並改寫自 **[Day 12: ES6篇: Side Effects(副作用)與Pure Functions(純粹函式)](https://ithelp.ithome.com.tw/articles/10185780)**
> 在 React 中其實處處可見到純粹函式的設計,但可能沒那麼顯眼,我們在其中都有使用到,只是身在其中有可能不自知。你可能沒注意到像React官網的這個網頁 [Components and Props](https://facebook.github.io/react/docs/components-and-props.html) 中就有這一句粗體的文字,這是在講解元件對於自己本身props的嚴格規則:
>
>
> > All React components must act like pure functions with respect to their props.
> 所有 React 元件對於它們的 props 都必須運作地像純粹函式。
> >
## 57. Monsters Rolodex - Hooks: useState
- 在 Functional component 中直接呼叫 `useState()`,並且傳入一個參數作為初始值。
- `useState()` 會回傳包含兩個值的陣列:
- 第一個值是當前 State 的狀態。(若都沒改變就是一開始傳入的初始值)
- 第二個值是一個函式,藉由傳入參數到這個函式並呼叫來改變初始值的值。
**補充:**
A side effect is when a function creates some kind of effect outside of its scope.
## 58. Monsters Rolodex - Functional Component Re-rendering 函式型元件重新渲染時機
函式型元件跟類別型元件重新渲染的條件是一樣的,如下面兩項:
- Props 值更新的時候
- State 值改變的時候
### 重要
1. 函式型元件呼叫 `useState` 的 `setXXX()` 去更新 state 值的時候,若值跟上一次一樣(物件型態的值就是看記憶體位址有無改變)就不會再次渲染,如有一 state 初始值為空字串 `''`,再用 `setXXX('')` 就不會重新渲染元件。而類別型元件則是一呼叫 `this.setState()` 就會重新渲染 `render()` 的內容!
2. 函式型元件會重新跑整個 function 內的內容,不像類別型元件只會渲染 `render()`!
## 59. Monsters Rolodex - Infinite Re-rendering 無窮渲染
**本小節重點:**
在寫函式型元件的時候,直接呼叫 `useState` 的 `setXXX()` 會造成無窮渲染 infinite loop,**這邊就算 `setXXX()` 的值與初始值相同也一樣會無窮渲染**,如下程式碼:
```jsx
import { useState } from 'react';
const App = () => {
const [searchString, setSearchString] = useState('');
const [monsters, setMonsters] = useState([]);
// 無窮渲染報錯 => 這邊就算 set 的值與初始值相同為空字串也一樣會
setSearchString('')
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((users) => {
// 無窮渲染報錯
setMonsters(users);
});
return (
<div className="App">
// 略...
</div>
);
};
export default App;
```
## 60. Monsters Rolodex - Hooks: useEffect

This picture is from 張至寧老師。
# 第 4 節: Capstone Project: Intro + Setup
本節只有首頁的 JSX 及一些基礎設置而已。
## 77. Adding Sass
在 React 中使用 .scss 檔案,安裝 Sass 套件
- `yarn add sass`
# 第 5 節: Routing + React-Router
本課程教的是 React-Router Version 6。
- `yarn add react-router-dom@6`
## 83. Setting Up Our Homepage
### 啟用 React-Router 6
1. 在 `index.js` `import { BrowserRouter } from 'react-router-dom';`
2. 用 `<BrowerRouter>` 把 `<App>` 包起來
```jsx
// 略...
import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
```
### 根目錄/src 下建置 routers 資料夾
這邊用來放置顯示層級是 top-level 的 component。
### 建置路由
1. 在 `App.js` `import { Routes, Route } from 'react-router-dom';`
2. `<Routes>` 裡面包 `<Route>`
- 本小節用到的 `<Route>` 的屬性如下,path 指到該路由就渲染 element 指定的 top-level component:
- `path=""`
- `element={}`
```jsx
import Home from './routes/home.component';
import { Routes, Route } from 'react-router-dom';
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
</Routes>
);
};
export default App;
```
## 84. React Router Outlet 巢狀路由
**本小節重點:**
React-Router 6 使用巢狀路由時,**當路由已符合父元件和子元件**,React 還是只會渲染父元件。要讓子元件也一併渲染的話,要在父元件中使用 `<Outlet />` 。
範例:
1. 當路由為 `[http://localhost:3000/home/shop](http://localhost:3000/home/shop)` ⇒ 符合父、子元件路由,目前只會顯示父元件:
```jsx
import Home from './routes/home.component';
import { Routes, Route } from 'react-router-dom';
const App = () => {
const Shop = () => {
return <h1>i am the shop page</h1>;
};
return (
<Routes>
<Route path="/home" element={<Home />}>
<Route path="shop" element={<Shop />} /> // 這邊的 path 不能寫 /shop 會報錯
</Route>
</Routes>
);
};
export default App;
```
1. 要在父元件 `<Home />` 中引用 `<Outlet />`,子元件就會渲染在 `<Outlet />` 的位置(也可以放在 `<Directory />` 上面)。
```jsx
import Directory from '../components/directory/directory.component';
import { Outlet } from 'react-router-dom';
const Home = () => {
return (
<div>
<Directory categories={categories} />;
<Outlet />
</div>
);
};
export default Home;
```
## 85. Navigation Bar Component 巢狀路由
**本小節重點:**
在巢狀路由下,當子元件想要跟隨父元件指定的路由一起渲染時(子元件自己不寫 path 屬性),可使用 index 屬性(即 `index={true}`)。
- 記得還是要用 Outlet 去告訴父元件要將子元件渲染在哪裡!
範例:
```jsx
import Home from './routes/home.component';
import { Routes, Route, Outlet } from 'react-router-dom';
const App = () => {
const Naigation = () => {
return (
<div>
<h1>i am the navigation page</h1>
<Outlet />
</div>
);
};
const Shop = () => {
return <h1>i am the shop page</h1>;
};
return (
<Routes>
<Route path="/" element={<Naigation />}>
<Route index element={<Home />} />
<Route path="shop" element={<Shop />} />
</Route>
</Routes>
);
};
export default App;
```
## 86. React Router Link 巢狀路由
**本小節重點:**
- `<Fragment>` 可以用來取代父級的 `<div>`,且不會渲染在畫面上。(PS 因元件只能 return 一個 tag )
- `<Link>` 同 `<a>` 標籤,用 `to="..."` 屬性指定連到哪個路由。
## 87. Styling for Navigation + Logo
**本小節重點:**
- 引用 SVG 圖片 ⇒ 用 import component 的方式
```jsx
import { ReactComponent as CrwnLogo } from '../../assets/crown.svg';
const Naigation = () => {
return (
<Fragment>
<div className="navigation">
<Link className="logo-container" to="/">
<CrwnLogo className="logo" />
</Link>
</Fragment>
);
};
export default Naigation;
```
# 第 7 節: React Context For State Management
### 105. User Context、106. Re-rendering From Context、107. Signing Out
Context 介紹
以下內容節錄自改寫 **[I Want To Know React - Context 語法](https://ithelp.ithome.com.tw/articles/10252519)、[初探 Context](https://ithelp.ithome.com.tw/articles/10252123)**
> Context 是一種利用向下廣播來傳遞資料的方式,此方法可以解決 props 必須要一層層向下傳遞的缺點。你可以將 context 視為一個用來儲存狀態的 component!
>
>
> ### **何時使用 Context**
>
> 然而並不是所有情境都適合使用 context。React 官方推薦只有在以下情境時,才使用 context。
>
> - 要把**全域性資料**傳遞給很多 components 時。所謂的全域性資料代表整個 App 都需要的資料,例如使用者資訊、時區設定、語系、UI 主題。
>
> ### **Context 角色**
>
> React context 的使用會環繞三個角色在運作:
>
> - Context Object
> - Provider
> - Consumer
>
> 一個 React app 中可以有多個 React context。每個 React context 的本體都是一個物件(在這邊把它稱為 context object)。其中 context object 中又會有兩個很重要的屬性:Provider(提供者)與 Consumer(消費者)。
>
> - Provider(提供者)的功用就是用來**提供** context 值。
> - Consumer(消費者)的功用則是用來**使用** context 值。
>
> 使用 Provider 的 component 與使用 Consumer 的 component 之間不需要是直接的父子層關係。Provider 只要在 Consumer 的上層即可讓 Consumer 接收到 context 值,而處於 Provider 與 Consumer 之間的中間層 component 則不須做任何的改動。
>
> ### **Context 使用步驟**
>
> 根據以上的資訊,我們可以把使用 Context 歸納成以下幾個步驟:
>
> 1. 創建 React context object
> 2. 在 Provider 中的 value 屬性放入值(即放值到 context object 中但不能直接取用),以將該值廣播給自己以下的 Consumer component 使用
> 3. Consumer 或者使用 useContext Hook 接收值,可根據接收到的值 component 可顯示對應的內容或執行對應的動作
- 關於 `createContext(defaultValue)`
> `defaultValue`:代表這個 context 的預設值,與 props 一樣,可為任意的值因為代表預設值,所以只有在 Consumer 以上的 component 中都沒有 Provider 時才會使用到 `defaultValue` 的內容。需要注意的是,如果 Consumer 上面有 Provider,但此 Provider 的值為 `undefined` 的話,則 Consumer 依然不會使用 `defaultValue`,拿到的值會是 `undefined`。
>
- 當 Provider 的 value 改變,則所有使用此 Provider 的 context object 的 Component 都會 re-run 但不一定會 re-redner!若 JSX 的部分都沒有更新就不會再 render。
### 本小節實作:
1. `src/components` 下新增 contexts 資料夾,底下再新增 `user.context.jsx`
```jsx
// 1. 引入 createContext
import { createContext, useState } from 'react';
// 2. 創建 Context 物件並填入預設值。 PS: Context 物件可以是純值
export const UserContext = createContext({
currentUser: null,
setCurrentUser: () => null,
});
// PS: 本課程講師將 <UserContext.Provider> 又再包裝成一個元件
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const value = { currentUser, setCurrentUser };
// 3. 將想要傳遞的值透過 .Provider 標籤的 value 屬性傳過去!
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
```
1. 用 `<UserContext.Provider>` 將 `<App>` 包覆 ⇒ Provider 的子元件可以取得 Provider 的 value(即 Context 物件)
```jsx
// in index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { UserProvider } from './components/contexts/user.context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<UserProvider>
<App />
</UserProvider>
</BrowserRouter>
</React.StrictMode>
);
```
1. 成功登入要把回傳的 user 資訊放到 Context 裡面,所以在 `sign-in-form.component.jsx` 操作
```jsx
// 有略過其他非相關程式碼...
// 1. 要引入 useContext 才能取用 Context
import { useState, useContext } from 'react';
// 2. 也要引用 Context 物件
import { UserContext } from '../contexts/user.context';
const SignInForm = () => {
// 3. 解構提取 setCurrentUser
const { setCurrentUser } = useContext(UserContext);
const handleSubmit = async (event) => {
event.preventDefault();
try {
const { user } = await signInAuthUserWithEmailAndPassword(
email,
password
);
// 4. 存取 user 資料
setCurrentUser(user);
setFormFields(defaultFormFields);
} catch (error) {
console.log(error);
}
}
};
return (
<div className="sign-up-container">
// 略...
</div>
);
};
export default SignInForm;
```
1. 第一次註冊成功後也要是登入狀態,所以 `sign-up-form.component.jsx` 也要做上述動作。
2. 取得登入資料後的 header 的 Sign In 按鈕要改成 Sign Out 並加入登出功能,以 UserContext 中有無 user 資訊作判別
- 在 `firebase.utils.js`
```jsx
import { signOut } from 'firebase/auth';
export const signOutUser = async () => await signOut(auth);
```
- 在 `navigation.component.jsx`
```jsx
import { Fragment } from 'react';
import { Outlet, Link } from 'react-router-dom';
import { ReactComponent as CrwnLogo } from '../../assets/crown.svg';
import { UserContext } from '../../components/contexts/user.context';
import { useContext } from 'react';
import { signOutUser } from '../../utils/firebase/firebase.utils';
const Naigation = () => {
const { currentUser, setCurrentUser } = useContext(UserContext);
const signOutHandler = async () => {
await signOutUser();
// 登出後 Context user 要清除才會顯示 Sign in
setCurrentUser(null);
};
return (
<Fragment>
<div className="navigation">
<Link className="logo-container" to="/">
<CrwnLogo className="logo" />
</Link>
<div className="nav-links-container">
<Link className="nav-link" to="/shop">
Shop
</Link>
{currentUser ? (
<span className="nav-link" onClick={signOutHandler}>
Sign Out
</span>
) : (
<Link className="nav-link" to="/auth">
Sign in
</Link>
)}
</div>
</div>
<Outlet />
</Fragment>
);
};
export default Naigation;
```
# 第 9 節: React Context Continued
## 112. New Shop Page 建立商品頁面
- `src/routes/shop/shop.component.jsx`
```jsx
import SHOP_DATA from '../../shop-data.json';
const Shop = () => {
return (
<div>
{SHOP_DATA.map(({ id, name }) => (
<div key={id}>
<h1>{name}</h1>
</div>
))}
</div>
);
};
export default Shop;
```
## 113. Products Context
**本小節重點:**
將假商品資料 array 放在 Context 中。
1. `src/contexts/` 新增 `products.context.jsx`
```jsx
// 1. 引入 createContext
import { createContext, useState } from 'react';
import PRODUCTS from '../shop-data.json';
// 2. 建立 Context 物件並給初始值
export const ProductsContext = createContext({
products: [],
});
export const ProductsProvider = ({ children }) => {
const [products, setProducts] = useState(PRODUCTS);
const value = { products };
return (
<ProductsContext.Provider value={value}>
{children}
</ProductsContext.Provider>
);
};
```
1. 用 `ProductsContext.Provider>` 包覆 `<App>`, `<UserProvider>` 和 `<ProductsProvider>` 放置的內外層位置取決於你要怎麼設計的程式碼,這邊課程講師是將 `<ProductsProvider>` 放在內層
```jsx
// in index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import { UserProvider } from './contexts/user.context';
import { ProductsProvider } from './contexts/products.context';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<UserProvider>
<ProductsProvider>
<App />
</ProductsProvider>
</UserProvider>
</BrowserRouter>
</React.StrictMode>
);
```
## 116. Toggle Cart Open 購物車彈窗功能
**本小節重點:**
點選 cart-icon 要讓購物車彈窗彈出,isCartOpen 放置在 Context 中。
1. `src/contexts` 新增 `cart.context.jsx`
```jsx
import { createContext, useState } from 'react';
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => {},
});
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false);
const value = { isCartOpen, setIsCartOpen };
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
```
1. `<CartContext.Provider>` 包覆 `<App>`
```jsx
// 略...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<UserProvider>
<ProductsProvider>
<CartProvider>
<App />
</CartProvider>
</ProductsProvider>
</UserProvider>
</BrowserRouter>
</React.StrictMode>
);
```
1. `navigation.component.jsx` 要取用 CartContext 中 isCartOpen 的值來決定要不要讓 cart-dropdown 出現:
```jsx
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
import { UserContext } from '../../contexts/user.context';
const Naigation = () => {
const { currentUser } = useContext(UserContext);
const { isCartOpen } = useContext(CartContext);
return (
<Fragment>
<div className="navigation">
{currentUser ? (
<span className="nav-link" onClick={signOutUser}>
Sign Out
</span>
) : (
<Link className="nav-link" to="/auth">
Sign in
</Link>
)}
<CartIcon />
</div>
{isCartOpen && <CartDropdown />}
</div>
<Outlet />
</Fragment>
);
};
export default Naigation;
```
1. `cart-icon.component.jsx` 點擊購物車 Icon 要觸發 setIsCartOpen
```jsx
import { ReactComponent as ShoppingIcon } from '../../assets/shopping-bag.svg';
import './cart-icon.styles.scss';
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
const CartIcon = () => {
const { isCartOpen, setIsCartOpen } = useContext(CartContext);
const toggleIsCartOpen = () => setIsCartOpen(!isCartOpen);
return (
<div className="cart-icon-container" onClick={toggleIsCartOpen}>
<ShoppingIcon className="shopping-icon" />
<span className="item-count">0</span>
</div>
);
};
export default CartIcon;
```
## 117.&118. Add To Cart Pt.1、Pt.2
**本小節重點:**
點選商品頁之加入購物車按鈕要顯示在 cart-dropdown 上**且將購物車中的 items 要放入 CartContext** 。
1. 先製作 cart-item 的 component:`src/components/cart-item/cart-item.component.jsx`
```jsx
// 簡易骨架尚未 styling
import './cart-item.styles.scss';
const CartItem = ({ cartItem }) => {
const { name, quantity } = cartItem;
return (
<div>
<h2>{name}</h2>
<span>{quantity}</span>
</div>
);
};
export default CartItem;
```
1. `cart.context.jsx`
```jsx
import { createContext, useState } from 'react';
// 3. 建立處理 cartItems array 的 function => 回傳一個處理好的 array
const cartItemsHandler = (cartItems, productToAdd) => {
const itemIsExisted = cartItems.find(
(cartItem) => cartItem.id === productToAdd.id
);
// 如果點擊加入購物車的商品已存在於購物車中
if (itemIsExisted) {
return cartItems.map((cartItem) =>
cartItem.id === productToAdd.id
// 已存在就數量加一,未存在直接回傳該商品
? { ...cartItem, quantity: cartItem.quantity + 1 }
: cartItem
);
}
// 如果點擊加入購物車的商品不存在於購物車中就加入並且新增 quantity 屬性
return [...cartItems, { ...productToAdd, quantity: 1 }];
};
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => {},
// 1. CartContenxt 新增 cartItems、addItemToCart 並給預設值
cartItems: [],
addItemToCart: () => {},
});
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false);
// 2. 新增 cartItems 的 state
const [cartItems, setCartItems] = useState([]);
// 4. 建立 addItemToCart function
const addItemToCart = (productToAdd) => {
// 新增 item => 調整 cartItems 的 state
setCartItems(cartItemsHandler(cartItems, productToAdd));
};
// 5. addItemToCart, cartItems 傳入 value 供子元件取用
const value = { isCartOpen, setIsCartOpen, addItemToCart, cartItems };
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
```
1. `product-card.component.jsx` 商品頁按鈕要 import 剛在 CartContext 寫好的 addItemToCart function:
```jsx
import './product-card.styles.scss';
import Button from '../button/button.component';
// 1. 前置 import
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
const ProductCard = ({ product }) => { // props.product
const { name, price, imageUrl } = product;
const { addItemToCart } = useContext(CartContext);
const addProductToCart = () => addItemToCart(product);
return (
<div className="product-card-container">
<img src={imageUrl} alt={name} />
<div className="footer">
<span className="name">{name}</span>
<span className="price">{price}</span>
</div>
// Button 綁上 onClick
<Button buttonType="inverted" onClick={addProductToCart}>
Add to card
</Button>
</div>
);
};
export default ProductCard;
```
1. `cart-dropdown.component.jsx` 裡頭的 cartItems 陣列是要從 CartContext 取出
```jsx
import Button from '../button/button.component';
import './cart-dropdown.styles.scss';
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
import CartItem from '../cart-item/cart-item.component';
const CartDropdown = () => {
const { cartItems } = useContext(CartContext);
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartItems.map((item) => (
<CartItem key={item.id} cartItem={item} />
))}
</div>
<Button>GO TO CHECKOUT</Button>
</div>
);
};
export default CartDropdown;
```
## 120. Cart Item Designs
**本小節重點:**
- 更新 `cart-item.component.jsx`
- Navigation 中 `cart-icon.component.jsx` 的購物包包要隨著購物車內容數量增減 ⇒ 新增 cartCount 至 CartContext。
```jsx
import { createContext, useState, useEffect } from 'react';
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => {},
cartItems: [],
addItemToCart: () => {},
// 1. 新增 cartCount 到 Context 的預設值
cartCount: 0,
});
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false);
const [cartItems, setCartItems] = useState([]);
// 2. 新增 cartCount 的 useState
const [cartCount, setCartCount] = useState(0);
// 3. 每當 cartItems 變動時就重新計算 cartCounts
useEffect(() => {
const newCartCounts = cartItems.reduce(
(total, cartItem) => total + cartItem.quantity,
0
);
setCartCount(newCartCounts);
}, [cartItems]);
const value = {
isCartOpen,
setIsCartOpen,
addItemToCart,
cartItems,
cartCount, // 4. 傳入 value
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
```
```jsx
// cart-icon.component.jsx 顯示 cartCount 的數量
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
const CartIcon = () => {
// 1. 從 CartContext 提取 cartCount
const { isCartOpen, setIsCartOpen, cartCount } = useContext(CartContext);
const toggleIsCartOpen = () => setIsCartOpen(!isCartOpen);
return (
<div className="cart-icon-container" onClick={toggleIsCartOpen}>
<ShoppingIcon className="shopping-icon" />
// 2. 顯示 cartCount
<span className="item-count">{cartCount}</span>
</div>
);
};
export default CartIcon;
```
## 121. Creating Checkout Page 結帳頁面製作
**本小節重點:**
- 結帳頁面製作、增加購物車 item 數量按鈕功能
- useNavigate
1. `src/routes/checkout/checkout.component.jsx`
```jsx
// 結帳頁面要從 CartContext 拿資料
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
const Checkout = () => {
const { cartItems, addItemToCart } = useContext(CartContext);
return (
<div>
<h1>I am the Checkout page</h1>
{cartItems.map((cartItem) => {
const { id, name, quantity } = cartItem;
return (
<div key={id}>
<h2>{name}</h2>
<span>{quantity}</span>
<br />
<span>decrement</span>
<br />
<span onClick={() => addItemToCart(cartItem)}>increment</span>
// PS: 不可在 onClick 直接呼叫 addItemToCart(cartItem) 這會並造成無窮渲染
</div>
);
})}
</div>
);
};
export default Checkout;
```
1. import 至 `App.js`
```jsx
const App = () => {
return (
<Routes>
<Route path="/" element={<Naigation />}>
<Route index element={<Home />} />
<Route path="shop" element={<Shop />} />
<Route path="auth" element={<Authentication />} />
<Route path="checkout" element={<Checkout />} />
</Route>
</Routes>
);
};
```
1. cart-dropdown 的 checkout 按鈕點選之後要連到 checkout 頁面 ⇒ useNavigate
```jsx
import { useNavigate } from 'react-router-dom';
const CartDropdown = () => {
const goToCheckoutPage = () => {
navigate('/checkout');
};
return (
<div className="cart-dropdown-container">
<div className="cart-items">
{cartItems.map((item) => (
<CartItem key={item.id} cartItem={item} />
))}
</div>
<Button onClick={goToCheckoutPage}>GO TO CHECKOUT</Button>
</div>
);
};
export default CartDropdown;
```
## 122. Checkout Item Pt. 1 購物車 item減少數量按鈕功能
- `cart.context.jsx` 新增 `removeItemFromCartHelper` 並在 `checkout.component.jsx` 使用
```jsx
import { createContext, useState, useEffect } from 'react';
const addItemToCartHelper = (cartItems, productToAdd) => {
// 在 cartItems 找找看要加入的商品是否在陣列中
const foundItem = cartItems.find(
(cartItem) => cartItem.id === productToAdd.id
);
// 若找到該 item quantity + 1,其餘 item 不變 (直接 return)
if (foundItem) {
return cartItems.map((cartItem) =>
cartItem.id === productToAdd.id
? { ...cartItem, quantity: cartItem.quantity + 1 }
: cartItem
);
}
// 沒找到就新增 => 複製原 cartItems 再合併 productToAdd 跟新增 quantity 屬性 & 值為 1
return [...cartItems, { ...productToAdd, quantity: 1 }];
};
const removeItemFromCartHelper = (cartItems, productToRemove) => {
// 找到要移除的 product
const foundItem = cartItems.find(
(cartItem) => cartItem.id === productToRemove.id
);
// foundItem quantity 等於 1 的話就要整個從 cartItems 陣列中刪除
if (foundItem.quantity === 1) {
return cartItems.filter((cartItem) => cartItem.id !== productToRemove.id);
}
// foundItem quantity 不等於 1 該 item quantity - 1,其餘 item 不變 (直接 return)
return cartItems.map((cartItem) =>
cartItem.id === productToRemove.id
? { ...cartItem, quantity: cartItem.quantity - 1 }
: cartItem
);
};
export const CartContext = createContext({
isCartOpen: false,
setIsCartOpen: () => {},
cartItems: [],
addItemToCart: () => {},
removeItemFromCart: () => {},
cartCount: 0,
});
export const CartProvider = ({ children }) => {
const [isCartOpen, setIsCartOpen] = useState(false);
const [cartItems, setCartItems] = useState([]);
const [cartCount, setCartCount] = useState(0);
useEffect(() => {
const newCartCounts = cartItems.reduce(
(total, cartItem) => total + cartItem.quantity,
0
);
setCartCount(newCartCounts);
}, [cartItems]);
const addItemToCart = (productToAdd) => {
setCartItems(addItemToCartHelper(cartItems, productToAdd));
};
const removeItemFromCart = (productToRemove) => {
setCartItems(removeItemFromCartHelper(cartItems, productToRemove));
};
const value = {
isCartOpen,
setIsCartOpen,
addItemToCart,
removeItemFromCart,
cartItems,
cartCount,
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
```
## 123. Checkout Item Pt.2 & 124. Checkout Item Pt.3
**本兩節重點:**
- 結帳頁面結構微調整
- `cart.context.jsx` 新增 `clearItemFromCart` method ⇒ 點選叉叉從購物車移除商品。此動作與 `addItemToCart` 及 `removeItemFromCart` 動作相似,不再多說明。
- 新增 `checkout-item.component.jsx` 並加上 `addItemToCart`、`removeItemFromCart` 和 `clearItemFromCart` onClick 事件
```jsx
import { useContext } from 'react';
import { CartContext } from '../../contexts/cart.context';
const CheckoutItem = ({ cartItem }) => {
const { name, imageUrl, price, quantity } = cartItem;
const { clearItemFromCart, addItemToCart, removeItemFromCart } =
useContext(CartContext);
const clearItemFromCartHandler = () => clearItemFromCart(cartItem);
const addItemToCartHandler = () => addItemToCart(cartItem);
const removeItemFromCartHandler = () => removeItemFromCart(cartItem);
return (
<div className="checkout-item-container">
<div className="image-container">
<img src={imageUrl} alt={`${name}`} />
</div>
<span className="name">{name}</span>
<span className="quantity">
<div className="arrow" onClick={removeItemFromCartHandler}>
❮
</div>
<span className="value">{quantity}</span>
<div className="arrow" onClick={addItemToCartHandler}>
❯
</div>
</span>
<span className="price">{price}</span>
<span className="remove-button" onClick={clearItemFromCartHandler}>
✕
</span>
</div>
);
};
export default CheckoutItem;
```
## 125. Cart Total
`cart.context.jsx` 新增 cartTotal,做法與 cartCount 一樣,不再多做說明。
# 第 13 節: Reducers
## useReducer 怎麼用?
1. 宣告一個 reducer function,它要回傳一個新的 state 物件,並接收兩個參數:
1. `state` 目前的 state 物件
2. `action` 也是一個物件,其屬性包含:
1. `type` 字串,具體說明此 action 的動作
2. `payload` 任意直,optional
2. 使用 `useReducer`,語法如下:
```jsx
const [state, dispatch] = useReducer(reducerFunction, state 初始值)
```
useReducer 接收兩個參數:
1. 我們宣告的 reducer function
2. state 的初始值(自己宣告)
useReducer 回傳兩個值:
1. `state` 物件
2. dispatch function 想要讓 reducer function 接收到一個 action,就要呼叫 `dispatch()` 並傳入 action 物件作為參數。
呼叫 `dispatch()` 就會 run reducer function,reducer function 會回傳一個新的 state 物件,整個函式型元件就會 re-rerun。
### useReducer 使用場合
當一個事件發生需要更新數個 state 時就很適合使用 useReducer。
## 145. User Reducer
**本小節重點:**
將 `user.context.jsx` 中的 useState 改為 useReducer。
```jsx
import { createContext, useReducer, useEffect } from 'react';
import {
onAuthStateChangedListener,
createUserDocumentFromAuth,
} from '../utils/firebase/firebase.utils';
export const UserContext = createContext({
currentUser: null,
setCurrentUser: () => null,
});
export const USER_ACTION_TYPE = {
SET_CURRENT_USER: 'SET_CURRENT_USER',
};
// 建立 reducer function
const userReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case USER_ACTION_TYPE.SET_CURRENT_USER:
return {
...state,
currentUser: payload,
};
// 出現非預期 type 則拋出錯誤
default:
throw new Error(`unhandled type ${type} in userReducer`);
}
};
const INITIAL_STATE = {
currentUser: null,
};
export const UserProvider = ({ children }) => {
useEffect(() => {
const unscribe = onAuthStateChangedListener((user) => {
if (user) {
createUserDocumentFromAuth(user);
}
setCurrentUser(user);
});
return unscribe;
}, []);
const [{ currentUser }, dispatch] = useReducer(userReducer, INITIAL_STATE);
// const { currentUser } = state
const setCurrentUser = (user) => {
dispatch({ type: USER_ACTION_TYPE.SET_CURRENT_USER, payload: user });
};
const value = { currentUser, setCurrentUser };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
```
## 146.、147.、148. Cart Reducer
**本小節重點:**
將 `cart.context.jsx` 中的 useState 改為 useReducer。
### useReducer 實際演練
1. 宣告 reducer 的 state 初始值
```jsx
const INITIAL_STATE = {
isCartOpen: false,
cartItems: [],
cartCount: 0,
cartTotal: 0,
};
```
1. 建立 reducer function,通常 reducer function 內會用到 switch 且 defaut case 為拋出錯誤,用來提示遇到非預期的 action type。
```jsx
// reducer function 起手式大致如下
const cartReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case 'SET_CART_ITEMS':
return {
...state,
...payload,
};
case 'SET_IS_CART_OPEN':
return {
...state,
isCartOpen: payload,
};
default:
throw new Error(`Unhadled typed of ${type} in cartReducer`);
}
};
```
1. 使用 `useReducer()` 並在需求處呼叫 `dispatch()` 傳入 action 物件
```jsx
const [state, dispatch] = useReducer(cartReducer, INITIAL_STATE);
const { cartItems, cartCount, cartTotal, isCartOpen } = state;
const updateCartItemsForReducer = (newCartItems) => {
const newCartCounts = newCartItems.reduce(
(total, cartItem) => total + cartItem.quantity,
0
);
const newCartTotal = newCartItems.reduce(
(total, cartItem) => total + cartItem.quantity * cartItem.price,
0
);
dispatch({
type: 'SET_CART_ITEMS',
payload: {
cartItems: newCartItems,
cartTotal: newCartTotal,
cartCount: newCartCounts,
},
});
};
const addItemToCart = (productToAdd) => {
const newCartItems = addItemToCartHelper(cartItems, productToAdd);
updateCartItemsForReducer(newCartItems);
};
const removeItemFromCart = (productToRemove) => {
const newCartItems = removeItemFromCartHelper(cartItems, productToRemove);
updateCartItemsForReducer(newCartItems);
};
const clearItemFromCart = (productToClear) => {
const newCartItems = clearItemFromCartHelper(cartItems, productToClear);
updateCartItemsForReducer(newCartItems);
};
const setIsCartOpen = (bool) => {
dispatch({ type: 'SET_IS_CART_OPEN', payload: bool });
};
```
1. 程式碼優化
- action types 應宣告成一個常數物件並利用「.」運算子取值,避免人為打字錯誤。
```jsx
const CART_ACTION_TYPES = {
SET_CART_ITEMS: 'SET_CART_ITEMS',
SET_IS_CART_OPEN: 'SET_IS_CART_OPEN',
};
```
- 每次呼叫 dispatch() 傳入的 action 物件,可另打造 createAction helper function 來避免人為打字疏失。
```jsx
// in src/utils/reducer/reducer.utils.js
export const createAction = (type, payload) => ({ type, payload });
```
```jsx
const updateCartItemsForReducer = (newCartItems) => {
const newCartCounts = newCartItems.reduce(
(total, cartItem) => total + cartItem.quantity,
0
);
const newCartTotal = newCartItems.reduce(
(total, cartItem) => total + cartItem.quantity * cartItem.price,
0
);
dispatch(
createAction(CART_ACTION_TYPES.SET_CART_ITEMS, {
cartItems: newCartItems,
cartTotal: newCartTotal,
cartCount: newCartCounts,
})
);
};
const setIsCartOpen = (bool) => {
dispatch(createAction(CART_ACTION_TYPES.SET_IS_CART_OPEN, bool));
};
```