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