React Hooks 是 React 16.8 新增的特性,它可讓你在不編寫 class 的狀況下使用 state 以及其它的 React 特性。html
首先來看一下使用 Hooks 編寫的代碼是什麼樣子的:react
import React, { useState, useEffect } from ‘react’;
export default () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count;
});
return (
<div> <p>Current count is: {count}</p> <button onClick={() => setCount(count + 1)}>increment count</button> </div>
);
};
複製代碼
這段代碼實現了一個簡單的計數器功能,點擊按鈕使 count 增長,同時使頁面標題的顯示與 count 的變化同步。這裏引入了 useState
與 useEffect
,咱們稍後再來介紹他們,先來對比一下與 class component 的區別:數組
import React, { Component } from ‘react’;
export default class extends Component {
state = {
count: 0,
};
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate() {
document.title = this.state.count;
}
render() {
return (
<div> <p>Current count is: {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> increment count </button> </div>
);
}
}
複製代碼
經過對比發現使用 Hooks 編寫代碼簡練多了,state 初始化及變動 state 的方式發生了改變,生命週期函數也不用再編寫了,Hooks 使咱們擁有了在函數組件裏使用 state 及生命週期的能力。React 的組件分類也進行了從新定義,之前是有狀態組件、無狀態組件,如今劃分爲了類組件及函數組件。瀏覽器
那麼接下來就來詳細分析一下 Hooks 的具體用法。性能優化
首先是 useState
,這個是幹什麼用的?useState
可讓咱們在函數組件內部使用 state 進行狀態管理,語法以下:ide
// state, 和 setState 能夠任意命名,好比 count, setCount
const [state, setState] = useState(initialState)
複製代碼
useState
接收一個初始的狀態 initialState
,它返回一個數組,進行解構後一個是 state,一個是更新 state 的函數 setState
,每次 setState
調用後,就會將組件的一次從新渲染加入隊列(從新渲染組件或者和其它的 setState 進行批量更新)。函數
initialState
做爲初始狀態,能夠是基本類型,也能夠是對象(例如{a:1}
)或者函數,它只在組件首次渲染時被用到,首次渲染後 state 與 initialState 相同。佈局
在每次從新渲染後,useState
都會返回最新的 state
。而 setState
就至關因而類組件中的 this.setState()
,可是 Hooks 裏的 setState 並不會進行新 state 與舊 state 的合併,而是直接覆蓋。性能
另外 useState()
接收的參數也是任意的,能夠是基本類型,也能夠是對象,還能夠傳入一個函數,在這個函數裏能夠拿到舊的 state,例如:學習
{/* <button onClick={() => setCount(count + 1)}>increment count</button> */}
<button onClick={() => setCount((oldCount) => oldCount + 1)}>
increment count
</button>
複製代碼
useState
也可使用屢次,進行多個狀態管理:
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
複製代碼
那麼 useEffect
又是幹什麼的呢?useEffect
可讓咱們在函數組件中進行反作用操做,好比獲取數據、DOM 操做、日誌記錄等都屬於反作用。
useEffect
能夠看作是 componentDidMount
, componentDidUpdate
, componentWillUnmount
這三個生命週期函數的組合。
例如在前面的例子中咱們使用 useEffect
來更新頁面標題:
useEffect(() => {
document.title = count;
});
複製代碼
每次使用 setCount
更新狀態,useEffect 裏的函數都執行了一次,這就至關因而在類組件中同時使用了 componentDidMount
及 componentDidUpdate
:
componentDidMount() {
document.title = this.state.count;
}
componentDidUpdate() {
document.title = this.state.count;
}
複製代碼
能夠看到,在兩個生命週期裏寫了一樣的邏輯,這無疑形成了代碼的冗餘,而 useEffect
則會在每次渲染以後都執行,包括首次渲染,這樣咱們把相似於示例中的相同操做編寫一次放在 useEffect 中便可。
對於上面示例中的反作用是無需清除的,可是還有些反作用是須要清除的,例如手動綁定的 DOM 事件。
在類組件中清除反作用能夠在 componentWillUnmount
生命週期方法中進行,那麼在 Hooks 裏該如何實現呢?
其實,useEffect
已經提供了清除的機制,每一個反作用均可以添加一個可選的返回函數,用來在組件卸載的時候進行清除操做,好比下面代碼:
import React, { useState, useEffect } from ‘react’;
export default () => {
const [size, setSize] = useState({ width: 0, height: 0 });
const handleResize = () => {
const win = document.documentElement.getBoundingClientRect();
setSize({ width: win.width, height: win.height });
};
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
return (
<div> <p>Current size is:</p> <p> width: {size.width}, height: {size.height} </p> </div>
);
};
複製代碼
另外使用 useEffect
還有一個問題,就是每次進行 setState 操做時,可能都會觸發反作用的執行, 即便更改的 state 和這個反作用並無關係,好比下面的代碼:
import React, { useState, useEffect } from ‘react’;
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState('red');
useEffect(() => {
console.log('count');
document.title = count;
});
return (
<div> <p style={{ color: color }}>Current count is: {count}</p> <button onClick={() => setCount(count + 1)}>increment count</button> <br /> <button onClick={() => setColor(color === ‘red’ ? ‘blue’ : ‘red’)}> switch color </button> </div>
);
};
複製代碼
咱們發如今改變顏色時,修改頁面標題的反作用也在反覆執行,這無疑形成了資源的浪費,更不是咱們想要的效果。固然,useEffect
給咱們提供了第二個參數,能夠對渲染進行控制,它接收一個數組做爲參數,能夠傳入多個值,咱們來對上面代碼進行改進:
useEffect(() => {
console.log('count');
document.title = count;
}, [count]);
複製代碼
咱們傳入了 [count]
做爲第二個參數,這樣在每次渲染時都會先對 count 的新值和舊值進行對比,只有變化的時候這個反作用纔會執行。
另外使用 Hooks 還須要遵循以下規則:
固然爲了保證這些規則,咱們可使用 ESLint 插件 eslint-plugin-react-hooks
進行約束。
首先看一下 Context 是怎麼幹什麼用的:Context 提供了一種跨層級傳遞數據的方式,有了它就無需在每層組件都手動傳遞 props 了。
經過下面例子回顧一下 Context API 的使用:
// 建立一個 Context 對象
const ThemeContext = React.createContext(‘light’);
const ContextAPIDemo = () => {
return (
// 每一個 Context 對象都會返回一個 Provider React 組件,
// 它接收一個 value 屬性,傳遞給消費組件,
// 容許消費組件訂閱 context 的變化
<ThemeContext.Provider value="dark">
<MiddleComponent />
</ThemeContext.Provider>
);
};
// 一箇中間組件,使用 Context 傳遞數據並不須要中間組件透傳
const MiddleComponent = () => {
return (
<div>
<ThemedButton />
</div>
);
};
class ThemedButton extends Component {
// 把建立的 ThemeContext 對象賦值給 contextType 靜態屬性,
// 這樣就能夠經過 this.context 訪問到 ThemeContext 裏面的數據
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
const Button = (props) => {
const btnBgColor = props.theme === ‘dark’ ? ‘#333’ : ‘white’;
return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
複製代碼
這裏使用 React.createContext()
建立了一個 ThemeContext
,而後經過 Provider 把數據傳遞給消費組件,即 ThemedButton
,最後在消費組件裏把建立的 ThemeContext
賦值給了 contextType
靜態屬性,這樣咱們在消費組件裏就能夠經過 this.context
的方式訪問數據了。
固然在消費組件裏還可使用 Consumer
來獲取 Context 的數據:
const ThemedButton = () => {
return (
<ThemeContext.Consumer>
{(theme) => <Button theme={theme} />}
</ThemeContext.Consumer>
);
};
複製代碼
Ok,作了簡單的回顧以後,就來看一下在 Hooks 裏該如何使用 Context。
接下來咱們使用 useContext
改造下上面的示例:
// 省略其它代碼…
const Button = () => {
const theme = useContext(ThemeContext);
const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
return <button style={{ backgroundColor: btnBgColor }}>Toggle Theme</button>;
};
複製代碼
改造後就徹底能夠不用 Consumer
或 contextType
靜態屬性了,哪裏須要哪裏就直接使用 useContext
便可。
useContext
接收一個 Context 對象做爲參數,返回的是經過 Provider 傳入的 value 數據。
因爲示例中一直是一個固定的 ThemeContet,若是須要一個動態的 Context 該怎麼辦?我麼能夠經過 Provider
的 value
傳入回調函數進行處理:
const ContextAPIDemo = () => {
const [theme, setTheme] = useState(initialState.theme);
const toggleTheme = (val) => {
setTheme(val === 'dark' ? 'light' : 'dark');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}> <MiddleComponent /> </ThemeContext.Provider> ); }; 複製代碼
而後在目標組件能夠經過 useContext
拿到回調函數,例如:
const Button = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
const btnBgColor = theme === ‘dark’ ? ‘#333’ : ‘white’;
return (
<button
style={{ backgroundColor: btnBgColor }}
onClick={() => toggleTheme(theme)}
>
Toggle Theme
</button>
);
};
複製代碼
提起 Reducer,用過 Redux 的同窗應該都不陌生,Reducer Hook 裏的 Reducer 其實跟 Redux 裏的 Reducer 是同一個意思,接收舊的 state,返回新的 state,形如:(state, action) => newState
。
useReducer
的基本語法是這樣的:
const [state, dispatch] = useReducer(reducer, initialArg, init);
複製代碼
它接收三個參數:reducer
, initialArg
及可選的 init
函數:
reducer
: 接收舊的 state,返回新的 state,形如:`(state, action) => newStateinitialArg
: 初始 stateinit
: 做爲一個函數傳入,惰性地初始化 state,state 將會被設置爲 init(initialArg)
,這樣能夠將計算 state 的邏輯提取到外部,同時若是有重置 state 的需求的話也會很方便下面就來看一下具體的用法:
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case ‘increment’:
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <button onClick={() => dispatch({ type: 'decrement' })}>-</button> <button onClick={() => dispatch({ type: ‘increment’ })}>+</button> </> ); }; 複製代碼
useMemo
的語法是這樣的:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製代碼
接收一個函數和依賴數組做爲參數,而後只在依賴項變化時纔會計算 memoizedValue
的值,基於這一點,咱們能夠做爲性能優化的一個手段。
例以下面代碼,咱們經過對 computedNum
這個計算方法添加了 useMemo
,把 count
做爲依賴,只有 count
變化時 computedNum
纔會計算,這樣能夠防止無關的操做也會引發函數的執行(若是不加,在點擊 change color
時,computedNum
也會反覆計算)。
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState(‘blue’);
const computedNum = useMemo(() => {
console.log('render when count change');
return count + 1;
}, [count]);
return (
<div> <p>Current color is: {color}</p> <p>Current count is: {count}</p> <p>Computed num is: {computedNum}</p> <button onClick={() => setCount(count + 1)}>increment count</button> <button onClick={() => setColor(color === ‘blue’ ? ‘green’ : ‘blue’)}> change color </button> </div>
);
};
複製代碼
固然,咱們也能夠用 useMemo
去包裹一個組件,從而實現相似 PureComponent
的效果,防止組件反覆的渲染。
<>
{useMemo(
() => (
<ColorDemo color={color} /> ), [color] )} <> 複製代碼
useCallback
用法以下:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
複製代碼
useCallback
返回了一個 memoized 回調函數,它跟 useMemo
相似,useCallback(fn, deps)
至關於 useMemo(() => fn, deps)
。它的第二個參數跟 useMemo
的同樣,傳入一個依賴項數組,只有依賴變化時纔會生成新的函數,不然一直是同一個函數,這在進行組件性能優化時很是有用,避免多餘的開銷。好比使用 props 傳遞函數時,使用 callback
能夠避免每次渲染都生成一個新的函數,示例以下:
let firstOnClick;
const Button = (props) => {
if (!firstOnClick) {
firstOnClick = props.onClick;
}
console.log(firstOnClick === props.onClick);
return <button onClick={props.onClick}>increment count</button>;
};
export default () => {
const [count, setCount] = useState(0);
const [color, setColor] = useState('blue');
// 不用 useCallback 包裹的話,點擊 change color 按鈕,Button 組件裏每次也都會生成新的函數
// const handleIncrementCount = () => {
// setCount(count + 1);
// };
// 使用 useCallback 包裹後,只有點擊 increment count 時 count 發生變化,纔會生成新的函數,
// 修改顏色並不會生成新函數
const handleIncrementCount = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div> <p>Current color is: {color}</p> <p>Current count is: {count}</p> <Button onClick={handleIncrementCount} /> <button onClick={() => setColor(color === 'blue' ? 'green' : 'blue')}> change color </button> </div> ); }; 複製代碼
useRef
會返回一個可變的 ref 對象,其.current
屬性被初始化爲傳入的參數(initialValue),返回的 ref 對象在組件的整個生命週期內保持不變。
一般它被用來獲取子組件(或 DOM 元素):
export default () => {
const btnRef = useRef(null);
useEffect(() => {
btnRef.current.addEventListener('click', () => {
alert('click me');
});
}, []);
return (
<div> <button ref={btnRef}>click</button> </div>
);
};
複製代碼
另外,因爲 useRef
在每次渲染時返回的都是同一個 ref 對象,所以能夠用 ref.current
保存一個變量,它不會隨着組件的從新渲染而受到影響,例以下面一個定時器的例子:
export default () => {
const [count, setCount] = useState(0);
const ref = useRef(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('interval');
setCount(++ref.current);
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div> <p>Current count is: {count}</p> </div>
);
};
複製代碼
同時,變動 .current
屬性也不會引發組件的從新渲染。
useImperativeHandle
可讓咱們在使用 ref 時自定義暴露給父組件的實例值,這樣能夠隱藏掉一些私有方法或屬性,下面是一個 useImperativeHandle
與 forwardRef
配合使用的例子:
function Input(props, ref) {
const [val, setVal] = useState(0);
useEffect(() => {
setVal(props.count);
}, [props.count]);
const clearInput = useCallback(() => {
setVal('');
}, []);
useImperativeHandle(ref, () => ({
clear: () => {
clearInput();
},
}));
return (
<input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
);
}
const FancyInput = forwardRef(Input);
export default () => {
const [count, setCount] = useState(0);
const fancyRef = useRef(null);
const handleClearInput = useCallback(() => {
fancyRef.current.clear();
}, []);
return (
<div>
<p>Current count is: {count}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
<hr />
<FancyInput ref={fancyRef} count={count} />
<button onClick={handleClearInput}>clear input</button>
</div>
);
};
複製代碼
useLayoutEffect
與 useEffect
用法相同,只不過 useLayoutEffect
會在全部 DOM 更新以後同步調用,可使用它讀取 DOM 佈局並同步觸發重渲染,可是仍是建議使用 useEffect
,以免阻塞 UI 更新。
這裏有個示例,使用useEffect
和 useLayoutEffect
會有明顯的差別:
const useLayoutEffectDemo = () => {
const [height, setHeight] = useState(100);
const boxRef = useRef(null);
useLayoutEffect(() => {
if (boxRef.current.getBoundingClientRect().height < 200) {
console.log('set height: ', height);
setHeight(height + 10);
}
}, [height]);
const style = {
width: '200px',
height: `${height}px`,
backgroundColor: height < 200 ? 'red' : 'blue',
};
return (
<div ref={boxRef} style={style}> useLayoutEffect Demo </div>
);
};
複製代碼
咱們至關於給盒子設置了粗糙的過渡變化,使用 useEffect
這種過渡是生效的,可是換成 useLayoutEffect
以後,過渡效果已經沒了,只會顯示最終的效果。也就是說在瀏覽器執行繪製以前,useLayoutEffect
內部的更新計劃將被同步刷新。
用於開發自定義 Hooks 調試使用,例如:
useDebugValue(
size.width < 500 ? '---- size.width < 500' : '---- size.width > 500'
);
複製代碼
能夠在 React DevTools 裏查看相關的信息輸出。
React Hooks 的強大,不只僅是由於官方的內置 Hooks,同時它還支持自定義 Hooks,提升了組件的複用性,從必定程度了取代了 HOC 和 render props 的複用方式。
自定義 Hook 須要以 use
開頭,其內部還能夠調用其餘的 Hook,自定義 Hook 不須要具備特殊的標識,咱們能夠自定義參數及返回值,下面是一個自定義 Hook 示例,自定了一個 useSize
,用來獲取窗口的大小。
import { useState, useCallback, useEffect } from 'react';
const getDomElInfo = () => document.documentElement.getBoundingClientRect();
const getSize = () => ({
width: getDomElInfo().width,
height: getDomElInfo().height,
});
export default function useSize() {
const [size, setSize] = useState(() => getSize());
const onChange = useCallback(() => {
setSize(() => getSize());
}, []);
useEffect(() => {
window.addEventListener('resize', onChange, false);
return () => {
window.removeEventListener('resize', onChange, false);
};
}, [onChange]);
return size;
}
複製代碼
使用這個自定義 Hook 也很簡單:
import useSize from './useSize';
export default () => {
const size = useSize();
return (
<div> <p> size: width-{size.width}, height-{size.height} </p> </div>
);
};
複製代碼
好了,關於 React Hooks 就介紹到這裏,它不只僅打破了原有的組件編寫方式,更是一種新的思惟, 很是值得學習。