React16.8 發(fā)布已經(jīng)很長世間, 這段時間項目不忙, 正好準(zhǔn)備使用 React Hooks 進(jìn)行重構(gòu)升級堂鲜。React Hooks 的特性是它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 的特性,下面來讓我們一起進(jìn)入 React Hooks的新特性實踐吧猾编!
React Hook 特性
- 完全可選的空免。 你無需重寫任何已有代碼就可以在一些組件中嘗試 Hook棚辽。但是如果你不想,你不必現(xiàn)在就去學(xué)習(xí)或使用 Hook疏橄。
- 100% 向后兼容的伟叛。 Hook 不包含任何破壞性改動。
- 現(xiàn)在可用淤击。 Hook 已發(fā)布于 v16.8.0
官方 10 種 React Hooks
1.useState
useState 可以讓我們在不編寫 class 的情況下使用 state,以此可以達(dá)到讓函數(shù)組建重新渲染
initialState 參數(shù)只會在組件的初始渲染中起作用匠抗,后續(xù)渲染時會被忽略。如果初始 state 需要通過復(fù)雜計算獲得污抬,則可以傳入一個函數(shù)汞贸,在函數(shù)中計算并返回初始的 state绳军,此函數(shù)只在初始渲染時被調(diào)用:
function useState<S = undefined>(): [
S | undefined,
Dispatch<SetStateAction<S | undefined>>
]
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props)
return initialState
})
const App = () => {
const [count, setCount] = useState(0) // number 類型
const [obj, setObject] = React.useState({
count: 0,
name: "alex",
}) // object 類型
const [todos, setTodos] = useState([{ text: "Learn Hooks" }]) // 數(shù)組類型
return (
<div>
<p>current value {obj.count}</p>
<button onClick={() => setCount({ ...obj, count: count + 1 })}>+</button>
<button onClick={() => setCount({ ...obj, count: count + 1 })}>-</button>
</div>
)
}
setState 可以局部的更新,但是 useState 必須把真?zhèn)€對象修改后的只丟進(jìn)去來進(jìn)行更新,useState 覆蓋式更新 setState 調(diào)合 Object.assign()
const App = () => {
const [name, setName] = React.useState('jack')
return (
<div>
<p>{name}</p>
<button onClick={() => setName('rose')}>SET NEW NAME</button>
</div>
)
}
const AppChild = React.memo({{ data }:{ data: string }}) => {
const [name, setName] = React.useState(data)
return (
<div>{name}---{data}</div>
)
}
const AppChild = React.memo({{ data }:{ data: string }}) => {
const [name, setName] = React.useState(data)
React.useEffect(() => {
setName(data)
}, [data])
return (
<div>{name}---{data}</div>
)
}
initialState 參數(shù)只會在組件的初始渲染中起作用, 可以使用 useEffect
2、9.useEffect矢腻、 useLayoutEffect
useEffect:
該 Hook 接收一個包含命令式门驾、且可能有副作用代碼的函數(shù)。
React 會等待瀏覽器完成畫面渲染之后才會延遲調(diào)用 useEffect多柑,因此會使得處理額外操作很方便奶是。
在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱竣灌、設(shè)置定時器聂沙、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因為這可能會產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性初嘹。
使用 useEffect 完成副作用操作及汉。賦值給 useEffect 的函數(shù)會在組件渲染到屏幕之后執(zhí)行。你可以把 effect 看作從 React 的純函數(shù)式世界通往命令式世界的逃生通道屯烦。
useLayoutEffect:
其函數(shù)簽名與 useEffect 相同坷随,但它會在所有的 DOM 變更之后同步調(diào)用 effect∽す辏可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染温眉。在瀏覽器執(zhí)行繪制之前,useLayoutEffect 內(nèi)部的更新計劃將被同步刷新迅脐。
useLayoutEffect 與 componentDidMount芍殖、componentDidUpdate 的調(diào)用階段是一樣的。但是谴蔑,我們推薦你一開始先用 useEffect,只有當(dāng)它出問題的時候再嘗試使用 useLayoutEffect.
function useEffect(effect: EffectCallback, deps?: DependencyList): void
let timer = null
const App = () => {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
document.title = "componentDidMount" + count
}, [count])
React.useEffect(() => {
timer = setInterval(() => {
setCount((prevCount) => prevCount + 1)
}, 1000)
// 一定注意下這個順序:
// 告訴react在下次重新渲染組件之后龟梦,同時是下次執(zhí)行上面setInterval之前調(diào)用
return () => {
document.title = "componentWillUnmount"
clearInterval(timer)
}
}, [])
return (
<div>
Count: {count}
<button onClick={() => clearInterval(timer)}>clear</button>
</div>
)
}
- 比如第一個 useEffect 中,理解起來就是一旦 count 值發(fā)生改變隐锭,則修改 documen.title 值.
- 而第二個 useEffect 中傳遞了一個空數(shù)組[],這種情況下只有在組件初始化或銷毀的時候才會觸發(fā),用來代替 componentDidMount 和 componentWillUnmount 慎用.
- 還有另外一個情況,就是不傳遞第二個參數(shù),也就是 useEffect 只接收了第一個函數(shù)參數(shù),代表不監(jiān)聽任何參數(shù)變化.每次渲染 DOM 之后,都會執(zhí)行 useEffect 中的函數(shù),類似替代 componentDidUpdate.
注意點
- useEffect 里面使用到的 state 的值, 固定在了 useEffect 內(nèi)部, 不會被改變计贰,除非 useEffect 刷新钦睡,重新固定 state 的值
- useEffect 不能被判斷包裹
if (x < y) {
React.useEffect(() => {
document.title = "hello world"
})
}
- 不能被打斷, 在函數(shù)中提前使用 return, 會提前結(jié)束躁倒,不會去執(zhí)行 return 之后的代碼
const App = () => {
return <div>hello world</div>
React.useEffect(() => {
document.title = "hello world"
})
}
3.useContext
接收一個 context 對象(React.createContext 的返回值)并返回該 context 的當(dāng)前值荞怒。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <MyContext.Provider> 的 value prop 決定。
當(dāng)組件上層最近的 <MyContext.Provider> 更新時秧秉,該 Hook 會觸發(fā)重渲染褐桌,并使用最新傳遞給 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate象迎,也會在組件本身使用 useContext 時重新渲染荧嵌。
如果你在接觸 Hook 前已經(jīng)對 context API 比較熟悉呛踊,那應(yīng)該可以理解,useContext(MyContext) 相當(dāng)于 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>啦撮。
useContext(MyContext) 只是讓你能夠讀取 context 的值以及訂閱 context 的變化谭网。你仍然需要在上層組件樹中使用 <MyContext.Provider> 來為下層組件提供 context。
function useContext<T>(
context: Context<T> /*, (not public API) observedBits?: number|boolean */
): T
const colorContext = React.createContext("gray")
const Bar = () => {
// useContext 的參數(shù)必須是 context 對象本身
const color = React.useContext(colorContext)
return <div>{color}</div>
}
const Foo = () => <Bar />
const App = () => {
return (
<colorContext.Provider value={"red"}>
<Foo />
</colorContext.Provider>
)
}
useContext 可以解決 Consumer 多狀態(tài)嵌套的問題
4.useReducer
useState 的替代方案赃春。它接收一個形如 (state, action) => newState 的 reducer愉择,并返回當(dāng)前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話织中,就已經(jīng)知道它如何工作了薄辅。)
在某些場景下,useReducer 會比 useState 更適用抠璃,例如 state 邏輯較復(fù)雜且包含多個子值站楚,或者下一個 state 依賴于之前的 state 等。并且搏嗡,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化窿春,因為你可以向子組件傳遞 dispatch 而不是回調(diào)函數(shù) 。
function useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
initializer?: undefined
): [ReducerState<R>, Dispatch<ReducerAction<R>>]
interface IState{
count: number
}
interface IAction{
type: 'increment' | 'decrement'
}
const initialState: IState = { count: 0 }
const init = (initialCount) => {
return { count: 0 }
}
function reducer(state: IState, action: IAction){
switch(action.type){
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count:state.count - 1 }
default:
throw new Error()
}
}
const App = () => {
const [state, dispatch] = React.useReducer(reducer, initialState)
// 惰性傳值
const [state, dispatch] = React.useReducer(reducer, initialCount, init)
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch('decrement')}>-</button>
<button onClick={() => dispatch('increment')}>+</button>
<>
)
}
useReducer 可以直接傳值,也可以惰性傳值,惰性傳值可以達(dá)到重置,直接訪問外部 Reducer
如果 Reducer Hook 的返回值與當(dāng)前 state 相同采盒,React 將跳過子組件的渲染及副作用的執(zhí)行
5旧乞、6 useCallback、useMemo
useMemo 返回一個 memoized 值
useCallback 返回一個 memoized 回調(diào)函數(shù)磅氨。
當(dāng)傳入的依賴項改變時,函數(shù)才會執(zhí)行更新,當(dāng)你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時尺栖,它將非常有用
function useCallback<T extends (...args: any[]) => any>(
callback: T,
deps: DependencyList
): T
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T
useCallbacl(fn, deps) === useMemo(() => fn, deps)
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
useMemo 和 useCallback 實踐
const App = () => {
const [count, setCount] = React.useState(0)
const [name, setName] = React.useState("jack")
const [text, setText] = React.useState("hello world")
// 每次setCount時都會在內(nèi)存中生成一個新的地址去存儲name,所以發(fā)現(xiàn)應(yīng)用的地址不同,AppChild會重新渲染
const data = {
name: name === "jack" ? "rose" : "rose",
}
// 使用useMemo暫存name的值,只有當(dāng)name變化時才data才會在內(nèi)存中生成一個新的地址去存儲name,在去重新渲染
const data = React.useMemo(() => {
return {
name: name === "jack" ? "rose" : "jack",
}
}, [name])
// 同理
const handleOnChange = (e: Event) => {
setText(e.target.value)
}
const handleOnChange = React.useCallback((e: Event) => {
setText(e.target.value)
})
return (
<div>
<p>{count}</p>
<p>{name}</p>
<button onClick={() => setCount(count - 1)}>decrement</button>
<button onClick={() => setName("jack")}>set new name</button>
<AppChild text={text} data={data} onChange={handleOnChange} />
</div>
)
}
interface IFunctionProps {
text: string
data: { name: string }
onChange: (e: Event) => void
}
// React.memo 淺比較
const AppChild = React.memo(({ data, text, onChange }: IFunctionProps) => {
return (
<div>
<p>{data.name}</p>
<input value={text} onChange={onChange} />
</div>
)
})
useCallback 作用于緩存一個函數(shù),useMemo 作用于緩存計算后的值
7 useRef
useRef 返回一個可變的 ref 對象烦租,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)延赌。返回的 ref 對象在組件的整個生命周期內(nèi)保持不變。
useRef 就像是可以在其 .current 屬性中保存一個可變值的"盒子"
ref 這是一個普通 Javascript 對象叉橱。而 useRef() 和自建一個 {current: ...} 對象的唯一區(qū)別是挫以,useRef 會在每次渲染時返回同一個 ref 對象。
當(dāng) ref 對象內(nèi)容發(fā)生變化時窃祝,useRef 并不會通知你掐松。變更 .current 屬性不會引發(fā)組件重新渲染。如果想要在 React 綁定或解綁 DOM 節(jié)點的 ref 時運行某些代碼粪小,則需要使用回調(diào) ref 來實現(xiàn)大磺。
function useRef<T>(initialValue: T): MutableRefObject<T>
useEffect 里面的 state 的值,是固定的, 可以使用 useRef,作為全局變量
const App = () => {
const [count, setCount] = React.useState(0)
const countRef = React.useRef(0)
React.useEffect(() => {
const timer = setInterval(() => {
setCount(++countRef.current)
}, 1000)
return () => clearInterval(timer)
}, [])
return <div>{count}</div>
}
基本用法探膊,用來操作 dom
function TextInputWithFocusButton() {
const inputEl = useRef(null)
const onButtonClick = () => {
// `current` 指向已掛載到 DOM 上的文本輸入元素
inputEl.current.focus()
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
)
}
8 useImperativeHandle
useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父組件的實例值杠愧。在大多數(shù)情況下,應(yīng)當(dāng)避免使用 ref 這樣的命令式代碼突想。useImperativeHandle 應(yīng)當(dāng)與 forwardRef 一起使用
function useImperativeHandle<T, R extends T>(
ref: Ref<T> | undefined,
init: () => R,
deps?: DependencyList
): void
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
渲染 <FancyInput ref={inputRef} /> 的父組件可以調(diào)用 inputRef.current.focus()
10.useDebugValue
useDebugValue 可用于在 React 開發(fā)者工具中顯示自定義 hook 的標(biāo)簽殴蹄。
function useDebugValue<T>(value: T, format?: (value: T) => any): void
這個我用的比較少,就暫不贅敘究抓、有興趣可以查看官網(wǎng)的 demo
11.自定義 Hooks 定時器
const useIntervalTime = (callback, delay) => {
React.useEffect(() => {
if (delay !== null) {
const timer = setInterval(callback, delay)
return () => clearInterval(timer)
}
}, [delay])
}
const App = () => {
const [count, setCount] = React.useState(0)
useIntervalTime(() => {
setCount(count + 1)
}, 2000)
return <div>{count}</div>
}
此時你會發(fā)現(xiàn) count 顯示始終是 0,但是 setInterval 會一直引用舊的狀態(tài),count 值始終為 0
可以改為 setCount(++count),每一次定時到點去獲取最新的 count 賦值給 count,再去更新渲染
同樣也可以使用 useRef 全局作用域
const useIntervalTime = (callback, delay) => {
const saveCallback = React.useRef(0)
React.useEffect(() => {
saveCallback.current = callback
})
React.useEffect(() => {
if (delay !== null) {
const timer = setInterval(() => saveCallback.current(), delay)
return () => clearInterval(timer)
}
}, [delay])
}