Hook
Hook API 是一個新的概念,它允許您組合狀態和副作用。Hook 允許您在元件之間重複使用狀態邏輯。
如果您已經使用 Preact 一段時間,您可能熟悉「渲染道具」和「高階元件」等模式,這些模式試圖解決這些挑戰。這些解決方案往往使程式碼更難理解且更抽象。Hook API 使得可以清楚地提取狀態和副作用的邏輯,並且簡化了單元測試,使其可以獨立於依賴它的元件進行測試。
Hooks 可用於任何組件,並避免依賴於類別組件 API 的 this
關鍵字所帶來的許多陷阱。Hooks 依賴於閉包,而不是從組件實例存取屬性。這讓它們具有值繫結,並消除了處理非同步狀態更新時可能發生的許多陳舊資料問題。
有兩種方式可以匯入 Hooks:從 preact/hooks
或 preact/compat
。
簡介
了解 Hooks 最簡單的方法是將它們與等效的基於類別的組件進行比較。
我們將使用一個簡單的計數器組件作為範例,它會呈現一個數字和一個按鈕,按鈕會將數字增加 1
class Counter extends Component {
state = {
value: 0
};
increment = () => {
this.setState(prev => ({ value: prev.value +1 }));
};
render(props, state) {
return (
<div>
<p>Counter: {state.value}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
在 REPL 中執行現在,這裡有一個使用 Hooks 建立的等效函式組件
function Counter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]);
return (
<div>
<p>Counter: {value}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
在 REPL 中執行在這個時候它們看起來非常相似,但我們可以進一步簡化 Hooks 版本。
讓我們將計數器邏輯萃取到一個自訂 Hook,讓它可以輕鬆地在組件間重複使用
function useCounter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]);
return { value, increment };
}
// First counter
function CounterA() {
const { value, increment } = useCounter();
return (
<div>
<p>Counter A: {value}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// Second counter which renders a different output.
function CounterB() {
const { value, increment } = useCounter();
return (
<div>
<h1>Counter B: {value}</h1>
<p>I'm a nice counter</p>
<button onClick={increment}>Increment</button>
</div>
);
}
在 REPL 中執行請注意,CounterA
和 CounterB
彼此完全獨立。它們都使用 useCounter()
自訂 Hook,但每個 Hook 都有自己關聯狀態的實例。
覺得這看起來有點奇怪嗎?你並不孤單!
我們許多人花了一段時間才習慣這種方法。
相依關係引數
許多 hooks 接受一個引數,可用於限制何時應更新 hook。Preact 會檢查相依性陣列中的每個值,並檢查自上次呼叫 hook 以來它是否已變更。當未指定相依性引數時,hook 始終會執行。
在我們上述的 useCounter()
實作中,我們傳遞了一個相依性陣列給 useCallback()
function useCounter() {
const [value, setValue] = useState(0);
const increment = useCallback(() => {
setValue(value + 1);
}, [value]); // <-- the dependency array
return { value, increment };
}
在此傳遞 value
會導致 useCallback
在 value
變更時傳回一個新的函式參考。這是必要的,以避免「陳舊閉包」,其中回呼會始終參考建立時第一次渲染的 value
變數,導致 increment
始終設定為 1
的值。
這會在
value
變更時建立一個新的increment
回呼。基於效能考量,通常建議使用 回呼 來更新狀態值,而非使用相依性來保留目前的數值。
有狀態的 hooks
在此我們將看到如何將有狀態的邏輯引入函式元件。
在引入 hooks 之前,任何需要狀態的地方都必須使用類別元件。
useState
此 hook 接受一個引數,這將是初始狀態。呼叫此 hook 時,它會傳回一個包含兩個變數的陣列。第一個是目前的狀態,第二個是我們狀態的 setter。
我們的 setter 行為類似於經典狀態的 setter。它接受一個值或一個函式,其中 currentSate 為引數。
當您呼叫 setter 且狀態不同時,它會觸發重新渲染,從使用該 useState 的元件開始。
import { useState } from 'preact/hooks';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
// You can also pass a callback to the setter
const decrement = () => setCount((currentCount) => currentCount - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
)
}
在 REPL 中執行當我們的初始狀態很昂貴時,最好傳遞函式而非值。
useReducer
useReducer
鉤子與 redux 非常相似。與 useState 相比,當您有複雜的狀態邏輯(下一個狀態取決於前一個狀態)時,使用它會更容易。
import { useReducer } from 'preact/hooks';
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
function Counter() {
// Returns the current state and a dispatch function to
// trigger an action
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
}
在 REPL 中執行記憶化
在 UI 程式設計中,通常有一些狀態或結果的計算成本很高。記憶化可以快取該計算的結果,允許在使用相同輸入時重複使用它。
useMemo
使用 useMemo
鉤子,我們可以記憶化該計算的結果,並且僅在其中一個依賴項發生變更時才重新計算它。
const memoized = useMemo(
() => expensive(a, b),
// Only re-run the expensive function when any of these
// dependencies change
[a, b]
);
請勿在
useMemo
內部執行任何有效果的程式碼。副作用屬於useEffect
。
useCallback
useCallback
鉤子可確保在沒有依賴項變更的情況下,傳回的函式將保持引用相等。這可用於最佳化子元件的更新,當它們依賴引用相等來略過更新時(例如 shouldComponentUpdate
)。
const onClick = useCallback(
() => console.log(a, b),
[a, b]
);
有趣的事實:
useCallback(fn, deps)
等於useMemo(() => fn, deps)
。
useRef
若要取得功能元件內部 DOM 節點的參考,可以使用 useRef
鉤子。它的作用類似於 createRef。
function Foo() {
// Initialize useRef with an initial value of `null`
const input = useRef(null);
const onClick = () => input.current && input.current.focus();
return (
<>
<input ref={input} />
<button onClick={onClick}>Focus input</button>
</>
);
}
在 REPL 中執行請小心不要將
useRef
與createRef
混淆。
useContext
若要在功能元件中存取內容,我們可以使用 useContext
鉤子,而無需任何高階或包裝元件。第一個引數必須是從 createContext
呼叫建立的內容物件。
const Theme = createContext('light');
function DisplayTheme() {
const theme = useContext(Theme);
return <p>Active theme: {theme}</p>;
}
// ...later
function App() {
return (
<Theme.Provider value="light">
<OtherComponent>
<DisplayTheme />
</OtherComponent>
</Theme.Provider>
)
}
在 REPL 中執行副作用
副作用是許多現代應用程式的核心。無論您是要從 API 中擷取一些資料,還是觸發文件上的效果,您都會發現 useEffect
幾乎可以滿足您的所有需求。這是鉤子 API 的主要優點之一,它會將您的思維重塑為思考效果,而不是元件的生命週期。
useEffect
顧名思義,useEffect
是觸發各種副作用的主要方式。如果需要,您甚至可以從效果中傳回清除函式。
useEffect(() => {
// Trigger your effect
return () => {
// Optional: Any cleanup code
};
}, []);
我們將從一個 Title
元件開始,它應反映文件標題,以便我們可以在瀏覽器的標籤地址列中看到它。
function PageTitle(props) {
useEffect(() => {
document.title = props.title;
}, [props.title]);
return <h1>{props.title}</h1>;
}
useEffect
的第一個參數是一個無參數的回呼,用於觸發效果。在我們的案例中,我們只希望在標題真正變更時觸發它。如果標題保持不變,更新它就沒有意義。這就是我們使用第二個參數指定我們的 依賴陣列 的原因。
但有時我們有更複雜的使用案例。想像一個元件,它在掛載時需要訂閱一些資料,而在卸載時需要取消訂閱。這也可以使用 useEffect
來完成。為了執行任何清理程式碼,我們只需要在回呼中傳回一個函式即可。
// Component that will always display the current window width
function WindowWidth(props) {
const [width, setWidth] = useState(0);
function onResize() {
setWidth(window.innerWidth);
}
useEffect(() => {
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return <p>Window width: {width}</p>;
}
在 REPL 中執行清理函式是可選的。如果你不需要執行任何清理程式碼,則不需要在傳遞給
useEffect
的回呼中傳回任何內容。
useLayoutEffect
簽章與 useEffect 相同,但它會在元件相異且瀏覽器有機會繪製時立即觸發。
useErrorBoundary
每當子元件擲出錯誤時,你可以使用此掛勾來捕捉它並向使用者顯示自訂錯誤 UI。
// error = The error that was caught or `undefined` if nothing errored.
// resetError = Call this function to mark an error as resolved. It's
// up to your app to decide what that means and if it is possible
// to recover from errors.
const [error, resetError] = useErrorBoundary();
為了監控目的,通知服務任何錯誤通常非常有用。為此,我們可以利用一個可選的回呼,並將其作為第一個參數傳遞給 useErrorBoundary
。
const [error] = useErrorBoundary(error => callMyApi(error.message));
完整的用法範例可能如下所示
const App = props => {
const [error, resetError] = useErrorBoundary(
error => callMyApi(error.message)
);
// Display a nice error message
if (error) {
return (
<div>
<p>{error.message}</p>
<button onClick={resetError}>Try again</button>
</div>
);
} else {
return <div>{props.children}</div>
}
};
如果您過去使用過基於類別的組件 API,那麼這個掛勾本質上是 componentDidCatch 生命週期方法的替代方案。這個掛勾在 Preact 10.2.0 中引入。
實用程式掛勾
useId
這個掛勾會為每個呼叫產生一個唯一的識別碼,並保證在 伺服器 和用戶端渲染時,這些識別碼會保持一致。一致 ID 的常見使用案例是表單,其中 <label>
元素使用 for
屬性將它們與特定的 <input>
元素關聯起來。不過,useId
掛勾並不只限於表單,只要您需要唯一的 ID,就可以使用它。
為了讓掛勾保持一致,您需要在伺服器和用戶端都使用 Preact。
完整的用法範例可能如下所示
const App = props => {
const mainId = useId();
const inputId = useId();
useLayoutEffect(() => {
document.getElementById(inputId).focus()
}, [])
// Display a nice error message
return (
<main id={mainId}>
<input id={inputId}>
</main>
)
};
這個掛勾在 Preact 10.11.0 中引入,需要 preact-render-to-string 5.2.4。