說明
支持我們

Hook

Hook API 是一個新的概念,它允許您組合狀態和副作用。Hook 允許您在元件之間重複使用狀態邏輯。

如果您已經使用 Preact 一段時間,您可能熟悉「渲染道具」和「高階元件」等模式,這些模式試圖解決這些挑戰。這些解決方案往往使程式碼更難理解且更抽象。Hook API 使得可以清楚地提取狀態和副作用的邏輯,並且簡化了單元測試,使其可以獨立於依賴它的元件進行測試。

Hooks 可用於任何組件,並避免依賴於類別組件 API 的 this 關鍵字所帶來的許多陷阱。Hooks 依賴於閉包,而不是從組件實例存取屬性。這讓它們具有值繫結,並消除了處理非同步狀態更新時可能發生的許多陳舊資料問題。

有兩種方式可以匯入 Hooks:從 preact/hookspreact/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 中執行

請注意,CounterACounterB 彼此完全獨立。它們都使用 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 會導致 useCallbackvalue 變更時傳回一個新的函式參考。這是必要的,以避免「陳舊閉包」,其中回呼會始終參考建立時第一次渲染的 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 中執行

請小心不要將 useRefcreateRef 混淆。

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。