# KrypoCamp第四週實體課程
## 那些我們常見的 React 開發地雷
### 課前準備
- 新增 React 專案
```bash=
# 請把 new-project 改成自己的專案名稱
npx create-react-app@latest new-project
cd new-project
```
- 安裝 ethers.js, wallet connect
```bash=
npm install --save web3
npm install --save ethers
npm install --save @walletconnect/web3-provider
```
- 安裝 VSCode 插件: ES7+ React/Redux/React-Native snippets
- [上課 Demo](https://codepen.io/depresto/pen/OJQZmMp)
- [Counter](https://codepen.io/depresto/pen/eYVgvZy)
- [Counter2](https://codepen.io/depresto/pen/gOvgmgp)
- [作業連結](https://hackmd.io/@SVMGKOLoRDqczI3S4lKC9Q/ryvuaWG_q)
<!-- 先把 codepen 中的區塊鏈串接挖空 -->
### React hooks hell
#### 1. React hooks 介紹
- 在 React 16.8 前 Functional Component 無法儲存 state,使用性低
- React 16.8 推出 hooks API,使得 Functional Component 也具有生命週期
- 基礎 React hooks
- useState: 狀態儲存
```javascript=
const [variable, setVariable] = React.useState()
// 將 variable 更改為自己變數的名稱
// variable 為唯獨的 state
// 透過 setVariable 來更改 state
```
- useEffect: 生命週期
- 分為一個 arrow function 以及一個 dependency array
- dependency array 用來放要偵測變化的 state
- arrow function 會在 state 改變的時候執行
- 若 dependency array 為空,則會在 React Component 之後顯示
- 若不填入 dependency array,則會在任何 props 或是 state 有改變都會執行
- [Demo](https://codepen.io/depresto/pen/gOvgmgp)
```javascript=
const [variable, setVariable] = React.useState()
React.useEffect(() => {
// 這行會在 React Component 載入之後顯示
return () => {
// 這行會在 React Component 準備要移除之後顯示
}
}, [])
```
```javascript=
const [variable, setVariable] = React.useState()
React.useEffect(() => {
// 這行會在 variable 有改變之後顯示
return () => {
// 這行會在 variable 準備要改變之前顯示
}
}, [variable])
```
#### 2. 常見的 Dependency hell
把所有 ```props``` 與 ```function``` 都帶入 ```useEffect``` :
[錯誤1](https://codepen.io/depresto/pen/qBxoLaJ), [錯誤2](https://codepen.io/depresto/pen/gOveQwd), [正確](https://codepen.io/depresto/pen/OJQvrzN)
```jsx=
const BadPropsComponent = () => {
const currentUser = { id: 1 }
const [data, setData] = useState();
useEffect(() => {
fetch('https://userapi')
.then(response => response.json())
.then(data => {
const users = data.users
const user = users.find(user => user.id === currentUser.id)
setData(user)
})
console.log('錯誤示範');
}, [currentUser]);
return (
<div>{JSON.stringify(data)}</div>
)
}
```
```jsx=
const GoodPropsComponent = (props) => {
const currentUser = { id: 1 }
const [data, setData] = useState();
useEffect(() => {
fetch('https://userapi')
.then(response => response.json())
.then(data => {
const users = data.users
const user = users.find(user => user.id === currentUser.id)
setData(user)
})
console.log('正確示範');
}, [currentUser.id]);
return (
<div>{JSON.stringify(data)}</div>
)
}
const YetAnotherGoodPropsComponent = (props) => {
const [data, setData] = useState();
useEffect(() => {
const apiUrl = `https://example.com/api/data?page${props.page}&search=${props.search}`
axios.get(apiUrl).then(response => setData(response.data));
console.log('不盡然正確示範,但可以這樣解決');
}, [JSON.stringify(props)]);
return (
<div>{JSON.stringify(data)}</div>
)
}
```
為什麼?[JavaScript Object Comparison](https://stackoverflow.com/questions/1068834/object-comparison-in-javascript)
```javascript=
const obj1 = { a:1, b:2 }
const obj2 = { a:1, b:2 }
obj1 === obj2 // false
JSON.stringify(obj1) === JSON.stringify(obj2) // true
// 使用 lodash.js
_.isEqual(obj1, obj2) // true
```
```javascript=
const func1 = () => {}
const func2 = () => {}
func1 === func2 // false
```
根本原因: re-render 與 React hooks 生命週期
- 任何 props 與 state 的更新,就會使 React Component re-render
- 每當 re-render 完以後,就會再度觸發 useEffect
- useEffect 如果又造成 state 更新,就會再度造成 re-rerender
- 反覆多次之後,就會使 React 偵測到太多 re-render 事件,造成 React 崩潰
因此 React 就設計了 useCallback, useMemo, memo 等 hooks,來使 state 能存入 cache,減少 re-render
#### 3. Wrapper hell
```jsx=
const App = () => {
const [language, setLanguage] = useState('zh')
return (
<InsideComponent language={language} />
)
}
const InsideComponent = ({ language }) => {
return (
<AnotherInsideComponent language={language} />
)
}
const AnotherInsideComponent = ({ language }) => {
...
}
```
##### 使用 Context API 簡化
- Context API 是什麼
- Context API 想解決什麼問題?
```jsx=
// LanguageContextProvider.jsx
export const LanguageContext = React.createContext({ language: 'zh' })
const LanguageContextProvider = ({ children }) => {
const [language, setLanguage] = useState('zh')
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
)
}
export default LanguageContextProvider
// App.jsx
const App = () => {
return (
<LanguageContextProvider>
<InsideComponent />
</LanguageContextProvider>
)
}
// AnotherInsideComponent.jsx
import { LanguageContext } from '../contexts/LanguageContext'
const AnotherInsideComponent = ({ language }) => {
const { language, setLanguage } = React.useContext(LanguageContext)
...
}
```
#### 4. **Optional**, 忘了什麼時候該加入 useEffect dependencies
Vscode 是你的好朋友
- 開啟新的 React 專案
```bash=
npx create-react-app new-project
```
- 安裝 ES7+ React/Redux/React-Native snippets

### 重複利用 React hooks: [DEMO](https://codepen.io/depresto/pen/dydmwdB)
#### 1. 打包我們的 useEffect & useState
```javascript=
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const fetchData = useCallback(async () => {
setLoading(true)
const { data } = await axios.post(API_URL, { param1, param2 })
setData(data)
setLoading(false)
}, [param1, param2])
useEffect(() => {
fetchData()
}, [fetchData])
```
透過打包我們的 React hook 來重複利用(https://codepen.io/depresto/pen/dydmwdB)
```javascript=
// src/hooks/data.ts
export const useData = ({ param1, param2 }) => {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const fetchData = useCallback(async () => {
setLoading(true)
const { data } = await axios.post(API_URL, { param1, param2 })
setData(data)
setLoading(false)
}, [param1, param2])
useEffect(() => {
fetchData()
}, [fetchData])
return {
loading,
data,
refetch: fetchData,
}
}
```
使用方式
```jsx=
import { useData } from 'src/hooks/data'
const Component = (props) => {
const { loading, data, refetch } = useData({
param1: props.param1,
param2: props.param2
})
const onRefetch = () => {
refetch()
}
return (
<div>
{loading && <div>Loading...</div>}
<div>{data}</div>
<button onClick={onRefetch}>Refetch Data</button>
</div>
)
}
```
### 課堂練習
習題0:請用範例 Codepen 來與範例智能合約做串接
- 範例智能合約: https://rinkeby.etherscan.io/address/0xb8bb92d80ec0d15ecce740d1da016829ec513ee4#code
- 範例 Codepen https://codepen.io/depresto/pen/NWyywRW
## 合約互動大補帖
### 使用 Wallet connect
- 在手機上由於沒有 Metamask 的 Chrome 插件,因此需要使用 Wallet Connect 來連接錢包的 App
- 如何知道使用者是否有安裝 Metamask
```javascript=
// 底下這行在手機上就無法讓我們使用 web3 功能
if (!window.ethereum || window.ethereum.isMetaMask) {
alert('請安裝 Metamask');
}
```
- 請先至 [Infura](https://infura.io/) 申請免費 Infura ID
- 安裝 Wallet connect
```bash=
npm install --save web3 ethers @walletconnect/web3-provider
```
- 將 Wallet connect 透過 ethers.js,串接前端
```javascript=
import WalletConnectProvider from "@walletconnect/web3-provider";
import { ethers } from 'ethers'
let provider
if (!window.ethereum || window.ethereum.isMetaMask) {
const INFURA_ID = '改成申請到的 Infura ID'
provider = new WalletConnectProvider({
infuraId: INFURA_ID,
});
// 會在手機上跳出 QR code 以連結錢包
await provider.enable();
} else {
provider = new ethers.providers.Web3Provider(window.ethereum)
}
const contract = new ethers.Contract(
CONTRACT_ADDRESS,
CONTRACT_ABI,
provider,
{ gasLimit: GAS_LIMIT }
)
```
### 自動重新取得合約資訊
- 範例智能合約: https://rinkeby.etherscan.io/address/0xb8bb92d80ec0d15ecce740d1da016829ec513ee4#code
使用 ```window.setInterval```
```javascript=
window.setInterval(() => {
contract.counter().then(counter => {
console.log(counter) // 顯示為一個 BigNumber 物件
})
}, 1000)
// 每一秒向區塊鏈取得智能合約中 counter 變數的值
```
使用 React
```javascript=
const [counter, setCounter] = useState()
useEffect(() => {
let interval = window.setInterval(() => {
contract.counter().then(setCounter)
}, 1000)
return () => {
clearInterval(interval)
// 記得清除 interval
// 否則 re-render 時不會清掉這個 interval
}
}, [])
```
### 課堂練習
習題1:請根據範例,透過 setInterval 自動取得新的智能合約狀態
- 範例 Codepen https://codepen.io/depresto/pen/NWyywRW
### 在任何的 React.Component 都可以使用 ethers.js 合約物件
使用 React Context API
```jsx=
// LanguageContextProvider.jsx
import { ethers } from 'ethers'
export const BlockchainContext = React.createContext({
contract: null,
currentAccount: null,
})
const BlockchainContextProvider = ({ children }) => {
const [contract, setContract] = useState(null)
const [currentAccount, setCurrentAccount] = React.useState(null);
React.useEffect(() => {
const onAccounts = accounts => {
const [_account] = accounts;
setCurrentAccount(_account);
}
window.ethereum.request({ method: 'eth_requestAccounts' })
.then(onAccounts);
window.ethereum.on("accountsChanged", onAccounts)
}, [])
React.useEffect(() => {
if (currentAccount) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
provider.getBlock().then(block => {
const _contract = new ethers.Contract(contractAddress, contractABI, provider, {
gasLimit: block.gasLimit
});
setContract(_contract.connect(signer));
})
}
}, [currentAccount])
return (
<BlockchainContext.Provider value={{ contract, currentAccount }}>
{children}
</BlockchainContext.Provider>
)
}
export default LanguageContextProvider
```
在其他 Component 取得 contract 物件
```javascript=
const { contract } = useContext(BlockchainContext)
```
- 習題2: 將 Codepen 中的 Counter 變成獨立的 Component
- Codepen: https://codepen.io/depresto/pen/NWyywRW
- 記得活用 Context API
### 自動切換正式/測試鏈
取得 Network ID
```javascript=
const networkId = await ethereum.request({
method: "net_version",
});
/*
* 1. Ethereum mainnet
* 3. ropsten
* 4. rinkeby
* 56. BSC mainnet
* 97. BSC testnet
* 43114. Avalanche mainnet
* /
```
切換 Network ID
```javascript=
const NETWORK_ID = 1
window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: `0x${NETWORK_ID.toString(16)}` }],
});
// Metamask 會跳出切換區塊鏈的要求
```
- 習題3: 如果使用者不是在 Rinkeby 測試鏈上,要給出提示要使用者切換鏈
- Codepen: https://codepen.io/depresto/pen/NWyywRW
## 進階內容
### React hooks 效能優化
- 回到我們的 React hooks 範例
- function 在 React 中的概念
```jsx=
const BadFunctionInReactHooks = () => {
const [data, setData] = useState();
const fetchData = async () => {
const apiUrl = `https://example.com/api/data?page${props.page}&search=${props.search}`
const { data } = await axios.get(apiUrl)
return data
}
useEffect(() => {
fetchData().then(setData)
// 會反覆重跑無數次
}, [fetchData])
return (
<div>{JSON.stringify(data)}</div>
)
}
```
- useCallback
```jsx=
const GoodFunctionInReactHooks = () => {
const [data, setData] = useState();
const fetchData = useCallback(async () => {
const apiUrl = `https://example.com/api/data?page${props.page}&search=${props.search}`
const { data } = await axios.get(apiUrl)
return data
}, [props.page, props.search])
useEffect(() => {
fetchData().then(setData)
// 只會當 props 的數值改變才會有變化
}, [fetchData])
return (
<div>{JSON.stringify(data)}</div>
)
}
```
- 使用 React.memo 優化
```jsx=
const MemoGoodPropsComponent = (props) => {
const [data, setData] = useState();
useEffect(() => {
const apiUrl = `https://example.com/api/data?page${props.page}&search=${props.search}`
axios.get(apiUrl).then(response => setData(response.data));
console.log('也可以透過 React.memo 解決');
}, [props]);
return (
<div>{JSON.stringify(data)}</div>
)
}
export default React.memo(MemoGoodPropsComponent, (prev, next) => {
return prev.page === next.page && prev.search === next.search
})
```
- 使用 React.useMemo 優化
```jsx=
const UseMemoGoodPropsComponent = (props) => {
const [data, setData] = useState();
const apiUrl = React.useMemo(() =>
`https://example.com/api/data?page${props.page}&search=${props.search}`
, [props.page, props.search])
useEffect(() => {
axios.get(apiUrl).then(response => setData(response.data));
console.log('也可以透過 React.useMemo 解決');
}, [apiUrl]);
return (
<div>{JSON.stringify(data)}</div>
)
}
```