---
# System prepended metadata

title: KrypoCamp第四週實體課程

---

# 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
    
![Auto dependencies](https://i.imgur.com/xQF2mhJ.png)
    

### 重複利用 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>
      )
    }
```