# 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> ) } ```