推薦閱讀:
看人家 Typescript 和 React hooks 耍的溜的飛起,好羨慕啊~ 那來吧,這篇爽文從腦殼到jio乾地教你如何使用這兩大利器開始閃亮開發!✨
課前預知我覺得比較好的學習方式就是跟著所講的內容自行實現一遍,所以先啟個專案唄~
npx create-react-app hook-ts-demo --template typescript
在 src/App.tsx 內引用我們的案例元件,在 src/example.tsx 寫我們的案例元件。
函式式元件的使用~ 我們可以通過以下方式使用有型別約束的函式式元件:
import React from 'react'type UserInfo = { name: string, age: number,}export const User = ({ name, age }: UserInfo) => { return ( <div className="App"> <p>{ name }</p> <p>{ age }</p> </div> )}const user = <User name='vortesnail' age={25} />
也可以通過以下方式使用有型別約束的函式式元件:
import React from 'react'type UserInfo = { name: string, age: number,}export const User:React.FC<UserInfo> = ({ name, age }) => { return ( <div className="User"> <p>{ name }</p> <p>{ age }</p> </div> )}const user = <User name='vortesnail' age={25} />
上述程式碼中不同之處在於:
export const User = ({ name, age }: UserInfo) => {}export const User:React.FC<UserInfo> = ({ name, age }) => {}
使用函式式元件時需要將元件申明為React.FC型別,也就是 Functional Component 的意思,另外props需要申明各個引數的型別,然後通過泛型傳遞給React.FC。
雖然兩種方式都差不多,但我個人更喜歡使用 React.FC 的方式來建立我的有型別約束的函式式元件,它還支援 children 的傳入,即使在我們的型別中並沒有定義它:
export const User:React.FC<UserInfo> = ({ name, age, children }) => { return ( <div className="User"> <p>{ name }</p> <p>{ age }</p> <div> { children } </div> </div> )}const user = <User name='vortesnail' age={25}>I am children text!</User>
我們也並不需要把所有引數都顯示地解構:
export const User:React.FC<UserInfo> = (props) => { return ( <div className="User"> <p>{ props.name }</p> <p>{ props.age }</p> <div> { /* 仍可以拿到 children */ } { props.children } </div> </div> )}const user = <User name='vortesnail' age={25}>I am children text!</User>
好了,我們暫時知道上面這麼多,就可以開始使用我們的 hooks 了~
我將從三個點闡述如何結合 typescript 使用我們的 hooks :
為啥使用❓怎麼使用場景例舉useState為啥使用useState?可以讓函式式元件擁有狀態管理特性,類似 class 元件中的 this.state 和 this.setState ,但是更加簡潔,不用頻繁的使用 this 。
怎麼使用useState?const [count, setCount] = useState<number>(0)
場景舉例1.引數為基本型別時的常規使用:
import React, { useState } from 'react'const Counter:React.FC<{ initial: number }> = ({ initial = 0 }) => { const [count, setCount] = useState<number>(initial) return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count+1)}>加</button> <button onClick={() => setCount(count-1)}>減</button> </div> )}export default Counter
2.引數為物件型別時的使用:import React, { useState } from 'react'type ArticleInfo = { title: string, content: string}const Article:React.FC<ArticleInfo> = ({ title, content }) => { const [article, setArticle] = useState<ArticleInfo>({ title, content }) return ( <div> <p>Title: { article.title }</p> <section>{ article.content }</section> <button onClick={() => setArticle({ title: '下一篇', content: '下一篇的內容', })}> 下一篇 </button> </div> )}export default Article
在我們的引數為物件型別時,需要特別注意的是, setXxx 並不會像 this.setState 合併舊的狀態,它是完全替代了舊的狀態,所以我們要實現合併,可以這樣寫(雖然我們以上例子不需要):
setArticle({ title: '下一篇', content: '下一篇的內容', ...article})
useEffect為啥使用useEffect?
你可以把 useEffect 看做 componentDidMount , componentDidUpdate 和 componentWillUnmount 這三個函式的組合。
怎麼使用useEffect?useEffect(() => { ... return () => {...}},[...])
場景舉例1.每當狀態改變時,都要重新執行 useEffect 的邏輯:
import React, { useState, useEffect } from 'react'let switchCount: number = 0const User = () => { const [name, setName] = useState<string>('') useEffect(() => { switchCount += 1 }) return ( <div> <p>Current Name: { name }</p> <p>switchCount: { switchCount }</p> <button onClick={() => setName('Jack')}>Jack</button> <button onClick={() => setName('Marry')}>Marry</button> </div> )}export default User
2.即使每次狀態都改變,也只執行第一次 useEffect 的邏輯:useEffect(() => { switchCount += 1}, [])
3.根據某個狀態是否變化來決定要不要重新執行:
const [value, setValue] = useState<string>('I never change')useEffect(() => { switchCount += 1}, [value])
因為 value 我們不會去任何地方改變它的值,所以在末尾加了 [value] 後, useEffect 內的邏輯也只會執行第一次,相當於在 class 元件中執行了 componentDidMount ,後續的 shouldComponentUpdate 返回全部是 false 。
4.元件解除安裝時處理一些記憶體問題,比如清除定時器、清除事件監聽:useEffect(() => { const handler = () => { document.title = Math.random().toString() } window.addEventListener('resize', handler) return () => { window.removeEventListener('resize', handler) }}, [])
useRef為啥使用useRef?它不僅僅是用來管理 DOM ref 的,它還相當於 this , 可以存放任何變數,很好的解決閉包帶來的不方便性。
怎麼使用useRef?const [count, setCount] = useState<number>(0)const countRef = useRef<number>(count)
場景舉例1.閉包問題:
想想看,我們先點選 加 按鈕 3 次,再點 彈框顯示 1次,再點 加 按鈕 2 次,最終 alert 會是什麼結果?
import React, { useState, useEffect, useRef } from 'react'const Counter = () => { const [count, setCount] = useState<number>(0) const handleCount = () => { setTimeout(() => { alert('current count: ' + count) }, 3000); } return ( <div> <p>current count: { count }</p> <button onClick={() => setCount(count + 1)}>加</button> <button onClick={() => handleCount()}>彈框顯示</button> </div> )}export default Counter
結果是彈框內容為 current count: 3 ,為什麼?
當我們更新狀態的時候, React 會重新渲染元件, 每一次渲染都會拿到獨立的 count 狀態, 並重新渲染一個 handleCount 函式. 每一個 handleCount 裡面都有它自己的 count 。
** 那如何顯示最新的當前 count 呢?
const Counter = () => { const [count, setCount] = useState<number>(0) const countRef = useRef<number>(count) useEffect(() => { countRef.current = count }) const handleCount = () => { setTimeout(() => { alert('current count: ' + countRef.current) }, 3000); } //...}export default Counter
2.因為變更 .current 屬性不會引發元件重新渲染,根據這個特性可以獲取狀態的前一個值:
const Counter = () => { const [count, setCount] = useState<number>(0) const preCountRef = useRef<number>(count) useEffect(() => { preCountRef.current = count }) return ( <div> <p>pre count: { preCountRef.current }</p> <p>current count: { count }</p> <button onClick={() => setCount(count + 1)}>加</button> </div> )}
我們可以看到,顯示的總是狀態的前一個值:
3.操作 Dom 節點,類似 createRef():import React, { useRef } from 'react'const TextInput = () => { const inputEl = useRef<HTMLInputElement>(null) const onFocusClick = () => { if(inputEl && inputEl.current) { inputEl.current.focus() } } return ( <div> <input type="text" ref={inputEl}/> <button onClick={onFocusClick}>Focus the input</button> </div> )}export default TextInput
useMemo為啥使用useMemo?
從 useEffect 可以知道,可以通過向其傳遞一些引數來影響某些函式的執行。 React 檢查這些引數是否已更改,並且只有在存在差異的情況下才會執行此。
useMemo 做類似的事情,假設有大量方法,並且只想在其引數更改時執行它們,而不是每次元件更新時都執行它們,那就可以使用 useMemo 來進行效能優化。
怎麼使用useMemo?記住,傳入 useMemo 的函式會在渲染期間執行。請不要在這個函式內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo 。
function changeName(name) { return name + '給name做點操作返回新name'}const newName = useMemo(() => {\treturn changeName(name)}, [name])
場景舉例1.常規使用,避免重複執行沒必要的方法:
我們先來看一個很簡單的例子,以下是還未使用 useMemo 的程式碼:
import React, { useState, useMemo } from 'react'// 父元件const Example = () => { const [time, setTime] = useState<number>(0) const [random, setRandom] = useState<number>(0) return ( <div> <button onClick={() => setTime(new Date().getTime())}>獲取當前時間</button> <button onClick={() => setRandom(Math.random())}>獲取當前隨機數</button> <Show time={time}>{random}</Show> </div> )}type Data = { time: number}// 子元件const Show:React.FC<Data> = ({ time, children }) => { function changeTime(time: number): string { console.log('changeTime excuted...') return new Date(time).toISOString() } return ( <div> <p>Time is: { changeTime(time) }</p> <p>Random is: { children }</p> </div> )}export default Example
在這個例子中,無論你點選的是 獲取當前時間 按鈕還是 獲取當前隨機數 按鈕, <Show /> 這個元件中的方法 changeTime 都會執行。
但事實上,點選 獲取當前隨機數 按鈕改變的只會是 children 這個引數,但我們的 changeTime 也會因為子元件的重新渲染而重新執行,這個操作是很沒必要的,消耗了無關的效能。
使用 useMemo 改造我們的 <Show /> 子元件:
const Show:React.FC<Data> = ({ time, children }) => { function changeTime(time: number): string { console.log('changeTime excuted...') return new Date(time).toISOString() } const newTime: string = useMemo(() => { return changeTime(time) }, [time]) return ( <div> <p>Time is: { newTime }</p> <p>Random is: { children }</p> </div> )}
這個時候只有點選 獲取當前時間 才會執行 changeTime 這個函式,而點選 獲取當前隨機數 已經不會觸發該函式執行了。
2.你可能會好奇, useMemo 能做的難道不能用 useEffect 來做嗎?答案是否定的!如果你在子元件中加入以下程式碼:
const Show:React.FC<Data> = ({ time, children }) => {\t//... useEffect(() => { console.log('effect function here...') }, [time]) const newTime: string = useMemo(() => { return changeTime(time) }, [time]) \t//...}
你會發現,控制檯會列印如下資訊:
> changeTime excuted...> effect function here...
正如我們一開始說的:傳入 useMemo 的函式會在渲染期間執行。 在此不得不提 React.memo ,它的作用是實現整個元件的 Pure 功能:
const Show:React.FC<Data> = React.memo(({ time, children }) => {...}
所以簡單用一句話來概括 useMemo 和 React.memo 的區別就是:前者在某些情況下不希望元件對所有 props 做淺比較,只想實現區域性 Pure 功能,即只想對特定的 props 做比較,並決定是否區域性更新。
useCallback為啥使用useCallback?useMemo 和 useCallback 接收的引數都是一樣,都是在其依賴項發生變化後才執行,都是返回快取的值,區別在於 useMemo 返回的是函式執行的結果, useCallback 返回的是函式。
怎麼使用useCallback?useCallback(fn, deps) 相當於 useMemo(() => fn, deps)
function changeName(name) { return name + '給name做點操作返回新name'}const getNewName = useMemo(() => { return changeName(name)}, [name])
場景舉例
將之前 useMemo 的例子,改一下子元件以下地方就OK了:
const Show:React.FC<Data> = ({ time, children }) => { //... const getNewTime = useCallback(() => { return changeTime(time) }, [time]) return ( <div> <p>Time is: { getNewTime() }</p> <p>Random is: { children }</p> </div> )}
useReducer為什麼使用useReducer?有沒有想過你在某個元件裡寫了很多很多的 useState 是什麼觀感?比如以下:
const [name, setName] = useState<string>('')const [islogin, setIsLogin] = useState<boolean>(false)const [avatar, setAvatar] = useState<string>('')const [age, setAge] = useState<number>(0)//...複製程式碼
怎麼使用useReducer?
import React, { useState, useReducer } from 'react'type StateType = { count: number}type ActionType = { type: 'reset' | 'decrement' | 'increment'}const initialState = { count: 0 }function reducer(state: StateType, action: ActionType) { switch (action.type) { case 'reset': return initialState case 'increment': return { count: state.count + 1 } case 'decrement': return { count: state.count - 1 } default: return state }}function Counter({ initialCount = 0}) { const [state, dispatch] = useReducer(reducer, { count: initialCount }) return ( <div> Count: {state.count} <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </div> )}export default Counter
場景舉例:與 useContext 結合代替 Redux 方案,往下閱讀。
useContext為啥使用useContext?簡單來說 Context 的作用就是對它所包含的元件樹提供全域性共享資料的一種技術。
怎麼使用useContext?export const ColorContext = React.createContext({ color: '#1890ff' })const { color } = useContext(ColorContext)// 或export const ColorContext = React.createContext(null)<ColorContext.Provider value='#1890ff'> <App /></ColorContext.Provider>// App 或以下的所有子元件都可拿到 valueconst color = useContext(ColorContext) // '#1890ff'
場景舉例1.根元件註冊,所有子元件都可拿到註冊的值:
import React, { useContext } from 'react'const ColorContext = React.createContext<string>('')const App = () => { return ( <ColorContext.Provider value='#1890ff'> <Father /> </ColorContext.Provider> )}const Father = () => { return ( <Child /> )}const Child = () => { const color = useContext(ColorContext) return ( <div style={{ backgroundColor: color }}>Background color is: { color }</div> )}export default App
2.配合 useReducer 實現 Redux 的代替方案:import React, { useReducer, useContext } from 'react'const UPDATE_COLOR = 'UPDATE_COLOR'type StateType = { color: string}type ActionType = { type: string, color: string}type MixStateAndDispatch = { state: StateType, dispatch?: React.Dispatch<ActionType>}const reducer = (state: StateType, action: ActionType) => { switch(action.type) { case UPDATE_COLOR: return { color: action.color } default: return state }}const ColorContext = React.createContext<MixStateAndDispatch>({ state: { color: 'black' },})const Show = () => { const { state, dispatch } = useContext(ColorContext) return ( <div style={{ color: state.color }}> 當前字型顏色為: {state.color} <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'red'})}>紅色</button> <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'green'})}>綠色</button> </div> )}const Example = ({ initialColor = '#000000' }) => { const [state, dispatch] = useReducer(reducer, { color: initialColor }) return ( <ColorContext.Provider value={{state, dispatch}}> <div> <Show /> <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'blue'})}>藍色</button> <button onClick={() => dispatch && dispatch({type: UPDATE_COLOR, color: 'lightblue'})}>輕綠色</button> </div> </ColorContext.Provider> )}export default Example
以上此方案是值得好好思索的,特別是因為 TypeScript 而導致的型別約束! 當然,如果有更好的解決方案,希望有大佬提出來,我也可以多學習學習~