(給前端大全加星標,提升前端技能)
英文:usehooks 譯文:林林小輝
https://zhuanlan.zhihu.com/p/66170210
Hooks是React 16.8新增的一項特性,可以讓你在不使用class的情況下去使用state和React的其他功能。這篇文章提供了簡單易懂的案例,幫助你去了解hooks如何使用,並且鼓勵你在接下來的項目中去使用它。但是在此之前,請確保你已經看了hook的官方文檔
useEventListener如果你發現自己使用useEffect添加了許多事件監聽,那你可能需要考慮將這些邏輯封裝成一個通用的hook。在下面的使用竅門裡,我們創建了一個叫useEventListener的hook,這個hook會檢查addEventListener是否被支持、添加事件監聽並且在cleanup鉤子中清空監聽。你可以在CodeSandbox demo上查看在線實例。
import { useRef, useEffect, useCallback } from 'react';// 使用function App(){
// 用來儲存滑鼠位置的State
const [coords, setCoords] = useState({ x: 0, y: 0 });
// 利用useCallback來處理回調
// ... 這裡依賴將不會發生改變
const handler = useCallback(
({ clientX, clientY }) => {
// 更新坐標
setCoords({ x: clientX, y: clientY });
},
[setCoords]
);
// 使用自定義的hook添加事件
useEventListener('mousemove', handler);
return (
<h1>
The mouse position is ({coords.x}, {coords.y})
</h1>
);}// Hookfunction useEventListener(eventName, handler, element = global){
// 創建一個儲存處理方法的ref
const savedHandler = useRef();
// 當處理函數改變的時候更新ref.current的方法
// 這樣可以使我們的總是獲取到最新的處理函數
// 並且不需要在它的effect依賴數組中傳遞
// 並且避免有可能每次渲染重新引起effect方法
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(
() => {
// 確認是否支持addEventListener
const isSupported = element && element.addEventListener;
if (!isSupported) return;
// 創建一個調用儲存在ref中函數的事件監聽
const eventListener = event => savedHandler.current(event);
// 添加事件監聽
element.addEventListener(eventName, eventListener);
// 在cleanup的回調中,清除事件監聽
return () => {
element.removeEventListener(eventName, eventListener);
};
},
[eventName, element] // 當元素或者綁定事件改變時,重新運行
);};
donavon/use-event-listener - 這個庫可以作為這個hook的原始資源。
useWhyDidYouUpdate這個hook讓你更加容易觀察到是哪一個prop的改變導致了一個組件的重新渲染。如果一個函數運行一次的成本非常的高,並且你也知道它會因為哪些prop造成重複的渲染,你可以使用React.memo這個高階組件來解決這個問題,在接下來有一個Counter的組件將會使用這個特性。在這個案例中,如果你還在尋找一些看起來不必要的重新渲染,你可以使用useWhyDidYouUpdate這個hook,並且在你的控制臺查看哪一個prop在這次渲染中發生了改變和它改變前後的值。Pretty nifty huh? 你可以在這裡查看在線實例。CodeSandbox demo
import { useState, useEffect, useRef } from 'react';// 讓我們裝作這個<Counter>組件的重新渲染成本很高...// ... 我們使用React.memo將它包裹起來,但是我們仍然需要尋找性能問題 :/// 因此我們添加useWhyDidYouUpdate並在控制臺查看將會發生什麼const Counter = React.memo(props => {
useWhyDidYouUpdate('Counter', props);
return <div style={props.style}>{props.count}</div>;});function App() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(0);
// 我們的控制臺告訴了我們 <Counter> 的樣式prop...
// ... 在每一次重新渲染中的改變,即使我們只通過按鈕改變了userId的狀態 ...
// ... 這是因為每一次重新渲染中counterStyle都被重新創建了一遍
// 感謝我們的hook讓我們發現了這個問題,並且提醒我們或許應該把這個對象移到component的外部
const counterStyle = {
fontSize: '3rem',
color: 'red'
};
return (
<div>
<div className="counter">
<Counter count={count} style={counterStyle} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
<div className="user">
<img src={`http://i.pravatar.cc/80?img=${userId}`} />
<button onClick={() => setUserId(userId + 1)}>Switch User</button>
</div>
</div>
);}// Hookfunction useWhyDidYouUpdate(name, props) {
// 獲得一個可變的kef對象,我們可以用來存儲props並且在下一次hook運行的時候進行比較
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
// 獲取改變前後所有的props的key值
const allKeys = Object.keys({ ...previousProps.current, ...props });
// 使用這個對象去跟蹤改變的props
const changesObj = {};
// 通過key值進行循環
allKeys.forEach(key => {
// 判斷改變前的值是否和當前的一致
if (previousProps.current[key] !== props[key]) {
// 將prop添加到用來追蹤的對象中
changesObj[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
// 如果改變的props不為空,則輸出到控制臺
if (Object.keys(changesObj).length) {
console.log('[why-did-you-update]', name, changesObj);
}
}
// 最後將當前的props值保存在previousProps中,以供下一次hook進行的時候使用
previousProps.current = props;
});}
這個hook包含了,當你需要在你的網站添加一個黑暗模式的所有狀態邏輯。它利用localStorage去記住用戶選擇的模式、默認瀏覽器或者系統級別設置使用prefers-color-schema媒體查詢和管理.dark-mode的類名去在body上應用你自己的樣式。 這篇文章同樣能幫助你了解將hook組合起來的威力。將state中的狀態同步到localStorage中使用的是useLocalStoragehook。檢測用戶的黑暗模式偏好使用的useMeidahook。這兩個hook都是我們在其他案例中創建的,但是這裡我們將它們組合起來,使用相當少的行數創建一個非常有用的hook。It’s almost as if hooks bring the compositional power of React components to stateful logic! 🤯
// Usagefunction App() {
const [darkMode, setDarkMode] = useDarkMode();
return (
<div>
<div className="navbar">
<Toggle darkMode={darkMode} setDarkMode={setDarkMode} />
</div>
<Content />
</div>
);}// Hookfunction useDarkMode() {
// 使用我們useLocalStorage hook即使在頁面刷新後也能保存狀態
const [enabledState, setEnabledState] = useLocalStorage('dark-mode-enabled');
// 查看用戶是否已經為黑暗模式設置了一個瀏覽器或系統偏好
// usePrefersDarkMode hook 組合了 useMedia hook (查看接下來的代碼)
const prefersDarkMode = usePrefersDarkMode();
// If enabledState is defined use it, otherwise fallback to prefersDarkMode.
// 這允許用戶在我們的網站上覆蓋掉系統級別的設置
const enabled =
typeof enabledState !== 'undefined' ? enabledState : prefersDarkMode;
// 改變黑暗模式
useEffect(
() => {
const className = 'dark-mode';
const element = window.document.body;
if (enabled) {
element.classList.add(className);
} else {
element.classList.remove(className);
}
},
[enabled] // 只要當enabled改變時調用該方法
);
// 返回enabled的狀態和設置方法
return [enabled, setEnabledState];}// 組合useMedia hook去檢測黑暗模式的偏好// useMedia被設計成可以支持多種媒體查詢並且返回數值。// 感謝hook的組合,我們可以把這一塊的複雜性隱藏起來// useMedia的方法在接下來的文章中function usePrefersDarkMode() {
return useMedia(['(prefers-color-scheme: dark)'], [true], false);}
donavon/use-dark-mode - 這個鉤子一個更可配置的的實現,並且同步了不同瀏覽器tab和處理的SSR情況。為這篇文章提供了很多代碼和靈感。
useMedia這個hook讓你輕易可以在你的component邏輯裡使用媒體查詢。在我們的例子中,我們可以根據哪一個媒體查詢匹配到了當前屏幕的寬度,並渲染不同的列數。然後分配圖片在列中不同的位置以限制列的高度差(我們並不像希望某一列比剩下的都要長)。 你可以創建一個直接獲取屏幕寬度的hook,代替使用媒體查詢。但是這個方法會讓你更容易在JS和你的Stylesheet共享媒體查詢。這裡查看在線示例。
import { useState, useEffect } from 'react';function App() {
const columnCount = useMedia(
// 媒體查詢
['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
// 列數 (跟上方的媒體查詢數組根據下標相關)
[5, 4, 3],
// 默認列數
2
);
// 創建一個默認的列高度數組,以0填充
let columnHeights = new Array(columnCount).fill(0);
// 創建一個數組用來儲存每列的元素,數組的每一項為一個數組
let columns = new Array(columnCount).fill().map(() => []);
data.forEach(item => {
// 獲取高度最矮的那一項
const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
// 添加item
columns[shortColumnIndex].push(item);
// 更新高度
columnHeights[shortColumnIndex] += item.height;
});
// 渲染每一列和其中的元素
return (
<div className="App">
<div className="columns is-mobile">
{columns.map(column => (
<div className="column">
{column.map(item => (
<div
className="image-container"
style={{
// 根據圖片的長寬比例調整圖片容器
paddingTop: (item.height / item.width) * 100 + '%'
}}
>
<img src={item.image} alt="" />
</div>
))}
</div>
))}
</div>
</div>
);}// Hookfunction useMedia(queries, values, defaultValue) {
// 一個包含了是否匹配每一個媒體查詢的數組
const mediaQueryLists = queries.map(q => window.matchMedia(q));
// 根據匹配的媒體查詢取值的方法
const getValue = () => {
// 獲取第一個匹配的媒體查詢的下標
const index = mediaQueryLists.findIndex(mql => mql.matches);
// 返回相對應的值或者默認值
return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
};
// 匹配值的state和setter
const [value, setValue] = useState(getValue);
useEffect(
() => {
// 回調方法
// 注意:通過在useEffect外定義getValue ...
// ... 我們可以確定它又從hook的參數傳入的最新的值(在這個hook的回調第一次在mount的時候被創建)
const handler = () => setValue(getValue);
// 為上面每一個媒體查詢設置一個監聽作為一個回調
mediaQueryLists.forEach(mql => mql.addListener(handler));
// 在cleanup中清除監聽
return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
},
[] // 空數組保證了effect只會在mount和unmount時運行
);
return value;}
useMedia v1 - 這個小方法的原始方案,使用一個事件監聽瀏覽器的resize事件,效果也很好,但是只對屏幕寬度的媒體查詢有用。 Masonry Grid - useMedia v1的源碼。這個demo在圖片改變列數時使用react-spring進行動畫。
useLockBodyScroll有時候當一些特別的組件在你們的頁面中展示時,你想要阻止用戶滑動你的頁面(想一想modal框或者移動端的全屏菜單)。如果你看到modal框下的內容滾動尤其是當你打算滾動modal框內的內容時,這可能會讓人很困惑。這個hook解決了這個問題。在任意組件內使用這個hook,只有當然這個組件unmount的時候,頁面才會被解鎖滑動。在線實例
import { useState, useLayoutEffect } from 'react';// 使用function App(){
// modal框的state
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<button onClick={() => setModalOpen(true)}>Show Modal</button>
<Content />
{modalOpen && (
<Modal
title="Try scrolling"
content="I bet you you can't! Muahahaha 😈"
onClose={() => setModalOpen(false)}
/>
)}
</div>
);}function Modal({ title, content, onClose }){
// 調用hook鎖定body滾動
useLockBodyScroll();
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal">
<h2>{title}</h2>
<p>{content}</p>
</div>
</div>
);}// Hookfunction useLockBodyScroll() {
useLayoutEffect(() => {
// 獲取原始body的overflow值
const originalStyle = window.getComputedStyle(document.body).overflow;
//防止在mount的過程中滾動
document.body.style.overflow = 'hidden';
// 當組件unmount的時候解鎖滾動
return () => document.body.style.overflow = originalStyle;
}, []); // 空數組保證了effect函數只會在mount和unmount的時候運行}
How hooks might shape desgin systems built in React - 一篇非常棒,啟發了這個小方法的文章。他們版本的useLockBodyScroll hook接受一個切換參數從而對鎖定狀態提供更多控制。
useTheme這個hook幫助你簡單使用CSS變量動態的改變你的app的表現。你只需要簡單的在你文檔的根元素傳遞一個,你想用來更新並且hook更新的每一個變量包含鍵值對的CSS變量。這在你無法使用行內樣式(沒有偽類支持)以及在你們的主題樣式裡有太多方式排列(例如一個可以讓用戶定製他們的外觀形象的app應用)的情況下很有用。值得注意的是,許多css-in-js的庫支持動態的樣式,但是嘗試一下僅僅使用CSS變量和一個React hook來完成會是非常有趣的。下面的例子非常簡單,但是你可以想像一下主題對象是被存儲在state中或者從接口獲取的。一定要看看這個有趣的在線實例。
import { useLayoutEffect } from 'react';import './styles.scss'; // -> https://codesandbox.io/s/15mko9187// Usageconst theme = {
'button-padding': '16px',
'button-font-size': '14px',
'button-border-radius': '4px',
'button-border': 'none',
'button-color': '#FFF',
'button-background': '#6772e5',
'button-hover-border': 'none',
'button-hover-color': '#FFF'};function App() {
useTheme(theme);
return (
<div>
<button className="button">Button</button>
</div>
);}// Hookfunction useTheme(theme) {
useLayoutEffect(
() => {
// 循環這個主題對象
for (const key in theme) {
// 更新文檔根元素的css變量
document.documentElement.style.setProperty(`--${key}`, theme[key]);
}
},
[theme] // 只要當主題對象發行改變時才會再次運行
);}
CSS Variables and React - 一篇激發了這個小方法的博文,來自Dan Bahrami。
useSpring這個hook是react-spring的一部分,react-spring是一個可以讓你使用高性能物理動畫的庫。我試圖在這裡避免引入依賴關係,但是這一次為了暴露這個非常有用的庫,我要破例做一次。react-spring的優點之一就是允許當你使用動畫時完全的跳過React render的生命周期。這樣經常可以得到客觀的性能提升。在接下來的例子中,我們將渲染一行卡片並且根據滑鼠移過每一個卡片的位置應用spring動畫效果。為了實現這個效果,我們使用由一組將要變換的值組成的數組來調用useSpring hook。渲染一個動畫組件(由react-spring導出),用onMouseMove事件獲取滑鼠的位置。然後調用setAnimationProps(hook返回的函數)去更新。你可以閱讀下面的代碼的注釋,或者直接查看在線實例
import { useState, useRef } from 'react';import { useSpring, animated } from 'react-spring';// 展示一行卡片// Usage of hook is within <Card> component belowfunction App() {
return (
<div className="container">
<div className="row">
{cards.map((card, i) => (
<div className="column">
<Card>
<div className="card-title">{card.title}</div>
<div className="card-body">{card.description}</div>
<img className="card-image" src={card.image} />
</Card>
</div>
))}
</div>
</div>
);}function Card({ children }) {
// 我們使用這個ref來儲存從onMouseMove事件中獲取的元素偏移值和大小
const ref = useRef();
// 持續跟蹤這個卡片是否hover狀態,這樣我們可以確保這個卡片的層級在其他動畫上面
const [isHovered, setHovered] = useState(false);
// The useSpring hook
const [animatedProps, setAnimatedProps] = useSpring({
// 用來儲存這些值 [rotateX, rotateY, and scale] 的數組
// 我們使用一個組合的key(xys)來代替分開的key,這樣我們可以在使用animatedProps.xys.interpolate()去更新css transform的值
xys: [0, 0, 1],
// Setup physics
config: { mass: 10, tension: 400, friction: 40, precision: 0.00001 }
});
return (
<animated.div
ref={ref}
className="card"
onMouseEnter={() => setHovered(true)}
onMouseMove={({ clientX, clientY }) => {
// 獲取滑鼠X坐標相對卡片的位置
const x =
clientX -
(ref.current.offsetLeft -
(window.scrollX || window.pageXOffset || document.body.scrollLeft));
// 獲取滑鼠Y相對卡片的位置
const y =
clientY -
(ref.current.offsetTop -
(window.scrollY || window.pageYOffset || document.body.scrollTop));
// 根據滑鼠的位置和卡片的大小設置動畫的值
const dampen = 50; // 數字越小,旋轉的角度越小
const xys = [
-(y - ref.current.clientHeight / 2) / dampen, // rotateX
(x - ref.current.clientWidth / 2) / dampen, // rotateY
1.07 // Scale
];
// 更新動畫的值
setAnimatedProps({ xys: xys });
}}
onMouseLeave={() => {
setHovered(false);
// 還原xys的值
setAnimatedProps({ xys: [0, 0, 1] });
}}
style={{
// 當卡片被hover時我們希望它的層級在其他卡片之上
zIndex: isHovered ? 2 : 1,
// 處理css變化的函數
transform: animatedProps.xys.interpolate(
(x, y, s) =>
`perspective(600px) rotateX(${x}deg) rotateY(${y}deg) scale(${s})`
)
}}
>
{children}
</animated.div>
);}
這個hook可以非常簡單的將撤銷/重做功能添加到你的應用中。我們的案例是一個簡單的繪畫應用。這個例子將會生成一個網格塊,你可以單擊任何一個塊去改變它的顏色,並且通過使用useHistory hook,我們可以在canvas上撤銷、重做或者清除所有的更改。在線示例。在我們的hook中,我們將使用useRoducer來代替useState儲存數據,這些東西應該對任何使用過redux的人都非常的熟悉(查看更多useReducer相關信息盡在官方文檔)。這個hook複製了use-undo這個庫並有一些細微的變化。因此你可以直接通過npm去安裝和使用這個庫。
import { useReducer, useCallback } from 'react';// Usagefunction App() {
const { state, set, undo, redo, clear, canUndo, canRedo } = useHistory({});
return (
<div className="container">
<div className="controls">
<div className="title">👩🎨 Click squares to draw</div>
<button onClick={undo} disabled={!canUndo}>
Undo
</button>
<button onClick={redo} disabled={!canRedo}>
Redo
</button>
<button onClick={clear}>Clear</button>
</div>
<div className="grid">
{((blocks, i, len) => {
// 生成一個網格塊
while (++i <= len) {
const index = i;
blocks.push(
<div
// 如果state中的狀態為true則給這個塊添加active類名
className={'block' + (state[index] ? ' active' : '')}
// 根據點擊改變塊的狀態並合併到最新的state
onClick={() => set({ ...state, [index]: !state[index] })}
key={i}
/>
);
}
return blocks;
})([], 0, 625)}
</div>
</div>
);}// 初始化useReducer中的stateconst initialState = {
// 當我們每次添加新state時,用來儲存更新前狀態的數組
past: [],
// 當前的state值
present: null,
// 讓我們可以用使用重做功能的,future數組
future: []};// 根據action處理state的改變const reducer = (state, action) => {
const { past, present, future } = state;
switch (action.type) {
case 'UNDO':
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future]
};
case 'REDO':
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture
};
case 'SET':
const { newPresent } = action;
if (newPresent === present) {
return state;
}
return {
past: [...past, present],
present: newPresent,
future: []
};
case 'CLEAR':
const { initialPresent } = action;
return {
...initialState,
present: initialPresent
};
}};// Hookconst useHistory = initialPresent => {
const [state, dispatch] = useReducer(reducer, {
...initialState,
present: initialPresent
});
const canUndo = state.past.length !== 0;
const canRedo = state.future.length !== 0;
// 設置我們的回調函數
// 使用useCallback來避免不必要的重新渲染
const undo = useCallback(
() => {
if (canUndo) {
dispatch({ type: 'UNDO' });
}
},
[canUndo, dispatch]
);
const redo = useCallback(
() => {
if (canRedo) {
dispatch({ type: 'REDO' });
}
},
[canRedo, dispatch]
);
const set = useCallback(newPresent => dispatch({ type: 'SET', newPresent }), [
dispatch
]);
const clear = useCallback(() => dispatch({ type: 'CLEAR', initialPresent }), [
dispatch
]);
// 如果需要,同樣可以到處過去和未來的state
return { state: state.present, set, undo, redo, clear, canUndo, canRedo };};
xxhomey19/use-undo - 上面所借鑑的庫,同樣從hook中返回了previous和future的狀態,但是沒有一個清晰的action React useHistory hook - 另一種useHistory的實現方式。
useScript使用這個hook可以讓你非常簡單的動態加載外部scr的ipt並且知道它什麼時候加載完畢。當你需要依賴一個第三方庫,並且想要按需加載而不是在每一個頁面的頭部請求時,這個hook非常有用。在下面的例子中我們直到腳本加載完成前才會調用我們在script中聲明的方法。如果你有興趣了解一下這個高級組件時如何實現的,你可以看一下source of react-script-loader-hoc。我個人覺得它比這個hook的可讀性更高。另一個優勢是因為它更容易調用一個hook去加載多個不同的script,而不像這個高階組件的實現方式,我們使用添加多個src的字符串來支持這個功能。
import { useState, useEffect } from 'react';// Usagefunction App() {
const [loaded, error] = useScript(
'https://pm28k14qlj.codesandbox.io/test-external-script.js'
);
return (
<div>
<div>
Script loaded: <b>{loaded.toString()}</b>
</div>
{loaded && !error && (
<div>
Script function call response: <b>{TEST_SCRIPT.start()}</b>
</div>
)}
</div>
);}// Hooklet cachedScripts = [];function useScript(src) {
// 持續跟蹤script加載完成和失敗的狀態
const [state, setState] = useState({
loaded: false,
error: false
});
useEffect(
() => {
// 如果cachedScripts數組中存在這個src則代表另一個hook的實例加載了這個script,所以不需要再加載一遍
if (cachedScripts.includes(src)) {
setState({
loaded: true,
error: false
});
} else {
cachedScripts.push(src);
// 創建script標籤
let script = document.createElement('script');
script.src = src;
script.async = true;
// Script事件監聽方法
const onScriptLoad = () => {
setState({
loaded: true,
error: false
});
};
const onScriptError = () => {
// 當失敗時,將cachedScripts中移除,這樣我們可以重新嘗試加載
const index = cachedScripts.indexOf(src);
if (index >= 0) cachedScripts.splice(index, 1);
script.remove();
setState({
loaded: true,
error: true
});
};
script.addEventListener('load', onScriptLoad);
script.addEventListener('error', onScriptError);
// 將script添加到文檔中
document.body.appendChild(script);
// 在cleanup回調中清除事件監聽
return () => {
script.removeEventListener('load', onScriptLoad);
script.removeEventListener('error', onScriptError);
};
}
},
[src] // 只有當src改變時才會重新運行
);
return [state.loaded, state.error];}
react-script-loader-hoc - 同樣邏輯的HOC實現,可以用來比較。 useScript from palmerhq/the-platform - 類似的hook,但是使用了React Suspense來返回一個promise
useKeyPress使用這個hook可以輕易的監測當用戶在他們的鍵盤上輸入特殊的鍵值時。這個小竅門非常的簡單,並且我想給你們看這只需要很少的代碼,但我挑戰任何讀者看誰能創建一個更高級的版本。監測當多個鍵同時被按住會是一個很好的補充。加分項:還能檢測是否在按照指定順序輸入鍵值。
const happyPress = useKeyPress('h');
const sadPress = useKeyPress('s');
const robotPress = useKeyPress('r');
const foxPress = useKeyPress('f');
return (
<div>
<div>h, s, r, f</div>
<div>
{happyPress && '😊'}
{sadPress && '😢'}
{robotPress && '🤖'}
{foxPress && '🦊'}
</div>
</div>
);}// Hookfunction useKeyPress(targetKey) {
// 用來儲存持續追蹤是否有鍵被按下
const [keyPressed, setKeyPressed] = useState(false);
// 如果按下的鍵值是我們的目標值,將其設置為true
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
// 如果鬆開的鍵值是我們的目標值,將其設置為false
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
// 添加事件監聽
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
// 在cleanup中清除回調
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, []); // 空數組意味著只有在mount和unmout的時候才會運行
return keyPressed;}
useMultiKeyPress - 這個例子可以同時檢測多個鍵值。
useMemoReact內置了一個叫useMemo的hook,允許你緩存開銷大的方法避免它們在每一次render中都被調用。你可以簡單的只傳入函數和數組然後useMemo將會只有在其中一個輸入改變的情況才會重新計算。下面在我們的例子中有一個叫computeLetterCount的開銷成本大的函數(出於演示目的,我們通過包含 一個完全不必要的大循環來降低速度)。當前選中的單詞發生改變時,你會觀察到因為新的單詞它需要重新調用computeLetterCount方法而造成的延遲。我們還有一個計數器用來每一次按鈕被點擊時增加計數。當計數器增加時,你會發現在兩次渲染之前沒有延遲。這是因為computeLetterCount沒有被調用。輸入文字並沒有改變因此返回的是緩存值。或許你想看一下CodeSandbox上的實例。
import { useState, useMemo } from 'react';// Usagefunction App() {
// 計數器的state
const [count, setCount] = useState(0);
// 追蹤我們在數組中想要展示的當前單詞
const [wordIndex, setWordIndex] = useState(0);
// 我們可以瀏覽單詞和查看字母個數
const words = ['hey', 'this', 'is', 'cool'];
const word = words[wordIndex];
// 返回一個單詞的字母數量
// 人為的使它運行緩慢
const computeLetterCount = word => {
let i = 0;
while (i < 1000000000) i++;
return word.length;
};
// 緩存computeLetterCount,當輸入數組的值和上一次運行一樣的話,就會返回緩存的值
const letterCount = useMemo(() => computeLetterCount(word), [word]);
// 這個方法會是我們增加計數變得延遲,因為我們不得不等開銷巨大的方法重新運行。
//const letterCount = computeLetterCount(word);
return (
<div style={{ padding: '15px' }}>
<h2>Compute number of letters (slow 🐌)</h2>
<p>"{word}" has {letterCount} letters</p>
<button
onClick={() => {
const next = wordIndex + 1 === words.length ? 0 : wordIndex + 1;
setWordIndex(next);
}}
>
Next word
</button>
<h2>Increment a counter (fast ⚡️)</h2>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);}
這個hook允許對任何快速改變的值去抖動。去抖動的值只有當最新的值在指定時間間隔內useDebounce hook沒有被調用的情況下才會改變。比如在下面的例子中我們用來和useEffect配合使用,你可以很容易地確保類似API調用這樣的昂貴操作不會被頻繁調用。下面的實例,我們將使用漫威漫畫API進行搜索,並且通過使用useDebounce防止API每次按鍵都被調用而導致你被接口屏蔽。在線實例 , hook代碼和靈感來自https://github.com/xnimorz/use-debounce
import { useState, useEffect, useRef } from 'react';// Usagefunction App() {
// 搜索詞
const [searchTerm, setSearchTerm] = useState('');
// API搜索結果
const [results, setResults] = useState([]);
// 搜索狀態 (是否有正在等待的請求)
const [isSearching, setIsSearching] = useState(false);
// 對改變搜索詞去抖動,只有當搜索詞500毫秒內沒有發生改變時,才會返回最新的值
// 目標就是只有當用戶停止輸入時才會調用API,防止我們太過迅速頻繁的調用API
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// Effect for API call
useEffect(
() => {
if (debouncedSearchTerm) {
setIsSearching(true);
searchCharacters(debouncedSearchTerm).then(results => {
setIsSearching(false);
setResults(results);
});
} else {
setResults([]);
}
},
[debouncedSearchTerm] // 只有當去抖動後的搜索詞改變時才會調用
);
return (
<div>
<input
placeholder="Search Marvel Comics"
onChange={e => setSearchTerm(e.target.value)}
/>
{isSearching && <div>Searching ...</div>}
{results.map(result => (
<div key={result.id}>
<h4>{result.title}</h4>
<img
src={`${result.thumbnail.path}/portrait_incredible.${
result.thumbnail.extension
}`}
/>
</div>
))}
</div>
);}// API search functionfunction searchCharacters(search) {
const apiKey = 'f9dfb1e8d466d36c27850bedd2047687';
return fetch(
`https://gateway.marvel.com/v1/public/comics?apikey=${apiKey}&titleStartsWith=${search}`,
{
method: 'GET'
}
)
.then(r => r.json())
.then(r => r.data.results)
.catch(error => {
console.error(error);
return [];
});}// Hookfunction useDebounce(value, delay) {
// 存儲去抖動後的值
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// 在延遲delay之後更新去抖動後的值
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 如果值改變了取消timeout (同樣在delay改變或者unmount時生效)
// 這就是我們通過延遲間隔內值沒有被改變來達到防止值去抖動 清空timeout並且重新運行
return () => {
clearTimeout(handler);
};
},
[value, delay] // 只有當搜索值或者delay值發生改變時才會重新調用
);
return debouncedValue;}
這個hook允許你輕易的檢測一個元素是否在屏幕上可見,以及指定有多少元素應該被顯示在屏幕上。當用戶滾動到某個特定區域,非常適合懶加載圖片或者觸發動畫。
import { useState, useEffect, useRef } from 'react';// Usagefunction App() {
// 用來儲存我們想要檢測是否在屏幕中的元素
const ref = useRef();
// 調用hook並傳入ref和root margin
// 在這種情況下,只有當元素多大於300px的元素才會在屏幕上顯示
const onScreen = useOnScreen(ref, '-300px');
return (
<div>
<div style={{ height: '100vh' }}>
<h1>Scroll down to next section 👇</h1>
</div>
<div
ref={ref}
style={{
height: '100vh',
backgroundColor: onScreen ? '#23cebd' : '#efefef'
}}
>
{onScreen ? (
<div>
<h1>Hey I'm on the screen</h1>
<img src="https://i.giphy.com/media/ASd0Ukj0y3qMM/giphy.gif" />
</div>
) : (
<h1>Scroll down 300px from the top of this section 👇</h1>
)}
</div>
</div>
);
}
// Hook
function useOnScreen(ref, rootMargin = '0px') {
// 儲存元素是否可見的狀態
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
// 當observer回調觸發是更新狀態
setIntersecting(entry.isIntersecting);
},
{
rootMargin
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.unobserve(ref.current);
};
}, []); // 空數組確保只會在mount和unmount執行
return isIntersecting;
}
react-intersection-observer - 一個更加健壯和可配置的實現。
usePrevious一個經常會出現的問題是,當使用hook的時候我們如何獲取props和state之前的值。在React的class組件內我們有componentDidUpdate方法用來參數的形式來接收之前的props和state,或者你客戶更新一個實例變量(this.previous = value)並在稍後引用它以獲得之前的值。所以我們如何能在沒有生命周期方法或者實例存儲值的函數組件中做到這一點呢?Hook來救火。我們可以創造一個定製的hook,使用useRef hook在內部存儲之前的值。查看下面的例子和行內注釋。或者直接查看官方例子
import { useState, useEffect, useRef } from 'react';// Usagefunction App() {
const [count, setCount] = useState(0);
// 獲取更新前的值 (在上一次render中傳進hook)
const prevCount = usePrevious(count);
// 同時展示當前值和更新前值
return (
<div>
<h1>Now: {count}, before: {prevCount}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);}// Hookfunction usePrevious(value) {
// ref對象是一個通用容器其current屬性為可變的,並且可以容納任何值,類似與一個類上的實例屬性。
const ref = useRef();
// Store current value in ref
useEffect(() => {
ref.current = value;
}, [value]); // 只有當值改變時重新運行
// 返回更新前的值 (發生在useEffect更新之前)
return ref.current;}
這個hook允許你監測是否在一個特定元素外點擊。在接下來的例子中,我們使用它監測在modal框以外任何元素被點擊時,去關閉modal框。通過抽象這個邏輯到一個hook中,我們可以很容易將它使用在需要這種類似功能的組件中(下拉菜單、提示等)
import { useState, useEffect, useRef } from 'react';// Usagefunction App() {
// 創建一個ref,儲存我們要監測外部點擊的元素
const ref = useRef();
// modal框的邏輯
const [isModalOpen, setModalOpen] = useState(false);
// 調用hook,並傳入ref和外部點擊時要觸發的函數
useOnClickOutside(ref, () => setModalOpen(false));
return (
<div>
{isModalOpen ? (
<div ref={ref}>
👋 Hey, I'm a modal. Click anywhere outside of me to close.
</div>
) : (
<button onClick={() => setModalOpen(true)}>Open Modal</button>
)}
</div>
);
}
// Hook
function useOnClickOutside(ref, handler) {
useEffect(
() => {
const listener = event => {
// 元素內點擊不做任何事
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
},
// 將ref和處理函數添加到effect的依賴數組中
// 值得注意的一點是,因為在每一次render中被傳入的處理方法是一個新函數,這將會導致effect的callback和cleanup每次render時被1調用。
// 這個問題也不大,你可以將處理函數通過useCallback包裹起來然後再傳入hook中。
[ref, handler]
);
}
[Andarist/use-onclickoutside] - 類似邏輯的庫。如果你想要從github/npm上拉取一些東西,這個庫是一個不錯的選擇。
useAnimation這個hook允許你通過一個緩動函數去平滑的動畫任意值(linear elastic)。在例子中,我們調用useAnimation hook三次去讓三個不同的小球在不同的間隔時間完成動畫。作為額外的一點,我們也展示了如何組合hook是非常簡單的。我們的useAnimation hook不實際使用useState或者useEffect本身,而是使用useAnimationTimer hook將其包裹起來。將計時器相關邏輯從hook中抽離出來,讓我們的代碼可讀性更高並且可以在其他環節使用計時器邏輯。在線實例
import { useState, useEffect } from 'react';// Usagefunction App() {
// 在不同的啟動延遲去多次調用hook以獲得不同的動畫值
const animation1 = useAnimation('elastic', 600, 0);
const animation2 = useAnimation('elastic', 600, 150);
const animation3 = useAnimation('elastic', 600, 300);
return (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Ball
innerStyle={{
marginTop: animation1 * 200 - 100
}}
/>
<Ball
innerStyle={{
marginTop: animation2 * 200 - 100
}}
/>
<Ball
innerStyle={{
marginTop: animation3 * 200 - 100
}}
/>
</div>
);}const Ball = ({ innerStyle }) => (
<div
style={{
width: 100,
height: 100,
marginRight: '40px',
borderRadius: '50px',
backgroundColor: '#4dd5fa',
...innerStyle
}}
/>);// Hook function useAnimation(
easingName = 'linear',
duration = 500,
delay = 0) {
// useAnimationTimer在我們給定的時間內在每一幀調用useState,儘可能的使動畫更加的流暢
const elapsed = useAnimationTimer(duration, delay);
// 在0-1的時間範圍內指定持續時間的總量
const n = Math.min(1, elapsed / duration);
// 根據我們指定的緩動函數返回修改後的值
return easing[easingName](n);}// 一些緩動函數的地址:// https://github.com/streamich/ts-easing/blob/master/src/index.ts// 在這裡硬編碼或者引入依賴const easing = {
linear: n => n,
elastic: n =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: n => Math.pow(2, 10 * (n - 1))};function useAnimationTimer(duration = 1000, delay = 0) {
const [elapsed, setTime] = useState(0);
useEffect(
() => {
let animationFrame, timerStop, start;
// 在每一幀動畫所要執行的函數
function onFrame() {
setTime(Date.now() - start);
loop();
}
// 在下一個幀上調用onFrame()
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
// 設置一個timeout當持續時間超過時停止
timerStop = setTimeout(() => {
cancelAnimationFrame(animationFrame);
setTime(Date.now() - start);
}, duration);
// 開始循環
start = Date.now();
loop();
}
// 在指定的延遲後執行(defaults to 0)
const timerDelay = setTimeout(onStart, delay);
// Clean things up
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
},
[duration, delay] // 只有當持續時間和延遲改變時重新運行
);
return elapsed;}
一個真正常見的需求是獲取瀏覽器當前窗口的尺寸。這個hook返回包含寬高的對象。如果在伺服器端執行(沒有window對象),則寬度和高度的值將未定義。
import { useState, useEffect } from 'react';// Usagefunction App() {
const size = useWindowSize();
return (
<div>
{size.width}px / {size.height}px
</div>
);}// Hookfunction useWindowSize() {
const isClient = typeof window === 'object';
function getSize() {
return {
width: isClient ? window.innerWidth : undefined,
height: isClient ? window.innerHeight : undefined
};
}
const [windowSize, setWindowSize] = useState(getSize);
useEffect(() => {
if (!isClient) {
return false;
}
function handleResize() {
setWindowSize(getSize());
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // 空數組保證effect只會在mount和unmount執行
return windowSize;}
監測一個滑鼠是否移動到某個元素上。這個hook返回一個ref和一個布爾值,改值表示當前具有該ref的元素是否被hover。因此只需要將返回的ref添加到你想要監聽hover狀態的任何元素。
import { useRef, useState, useEffect } from 'react';// Usagefunction App() {
const [hoverRef, isHovered] = useHover();
return (
<div ref={hoverRef}>
{isHovered ? '😁' : '☹️'}
</div>
);}// Hookfunction useHover() {
const [value, setValue] = useState(false);
const ref = useRef(null);
const handleMouseOver = () => setValue(true);
const handleMouseOut = () => setValue(false);
useEffect(
() => {
const node = ref.current;
if (node) {
node.addEventListener('mouseover', handleMouseOver);
node.addEventListener('mouseout', handleMouseOut);
return () => {
node.removeEventListener('mouseover', handleMouseOver);
node.removeEventListener('mouseout', handleMouseOut);
};
}
},
[ref.current] // 只有當ref改變時才會重新調用
);
return [ref, value];}
將state中的數據同步到localstorage,以便頁面刷新的時候保存狀態。使用方法和useState類似,我們只要傳入一個localstorage的值,以便在頁面加載時默認使用該值,而不是指定的初始值。
import { useState } from 'react';// Usagefunction App() {
// 與useState相似,但是第一個參數是localstorage中的key值
const [name, setName] = useLocalStorage('name', 'Bob');
return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);}// Hookfunction useLocalStorage(key, initialValue) {
// State to store our value
// 將初始狀態傳給useState,這樣邏輯只會執行一次
const [storedValue, setStoredValue] = useState(() => {
try {
// 通過key值從localstorage中獲取值
const item = window.localStorage.getItem(key);
// 如果沒有返回初始值則解析儲存的json
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// 如果報錯了依舊返回初始值
console.log(error);
return initialValue;
}
});
// 返回useState的setter函數的包裝版本,該函數將新的值保存到localstorage中
const setValue = value => {
try {
// 允許值是一個函數,這樣我們就有了和useState一樣的api
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// 保存state
setStoredValue(valueToStore);
// 保存到localStorage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// 更高級實現的處理將會處理錯誤的情況
console.log(error);
}
};
return [storedValue, setValue];}
use-persisted-state - 一個更高級的實現,可以在不同tab和瀏覽器窗口之間同步。
覺得本文對你有幫助?請分享給更多人
關注「前端大全」加星標,提升前端技能
好文章,我在看❤️