钩子

¥Hooks

Hooks API 是一个新概念,允许你组合状态和副作用。钩子允许你在组件之间重用有状态逻辑。

¥The Hooks API is a new concept that allows you to compose state and side effects. Hooks allow you to reuse stateful logic between components.

如果你使用 Preact 一段时间,你可能会熟悉尝试解决这些挑战的 "渲染属性" 和 "高阶分量" 等模式。这些解决方案往往使代码更难以理解并且更加抽象。hooks API 可以巧妙地提取状态和副作用的逻辑,并且还简化了独立于依赖它的组件的逻辑单元测试。

¥If you've worked with Preact for a while, you may be familiar with patterns like "render props" and "higher order components" that try to solve these challenges. These solutions have tended to make code harder to follow and more abstract. The hooks API makes it possible to neatly extract the logic for state and side effects, and also simplifies unit testing that logic independently from the components that rely on it.

钩子可以在任何组件中使用,并避免类组件 API 依赖的 this 关键字的许多陷阱。钩子依赖于闭包,而不是从组件实例访问属性。这使得它们受值限制,并消除了处理异步状态更新时可能出现的许多旧的数据问题。

¥Hooks can be used in any component, and avoid many pitfalls of the this keyword relied on by the class components API. Instead of accessing properties from the component instance, hooks rely on closures. This makes them value-bound and eliminates a number of stale data problems that can occur when dealing with asynchronous state updates.

导入钩子有两种方法:从 preact/hookspreact/compat 开始。

¥There are two ways to import hooks: from preact/hooks or preact/compat.



介绍

¥Introduction

理解钩子的最简单方法是将它们与等效的基于类的组件进行比较。

¥The easiest way to understand hooks is to compare them to equivalent class-based Components.

我们将使用一个简单的计数器组件作为示例,它渲染一个数字和一个将其加一的按钮:

¥We'll use a simple counter component as our example, which renders a number and a button that increases it by one:

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>
    );
  }
}
Run in REPL

现在,这是一个用钩子构建的等效功能组件:

¥Now, here's an equivalent function component built with 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>
  );
}
Run in REPL

此时它们看起来非常相似,但是我们可以进一步简化 hooks 版本。

¥At this point they seem pretty similar, however we can further simplify the hooks version.

让我们将计数器逻辑提取到自定义钩子中,使其可以轻松地跨组件重用:

¥Let's extract the counter logic into a custom hook, making it easily reusable across components:

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>
  );
}
Run in REPL

请注意,CounterACounterB 彼此完全独立。它们都使用 useCounter() 自定义钩子,但每个都有自己的该钩子关联状态的实例。

¥Note that both CounterA and CounterB are completely independent of each other. They both use the useCounter() custom hook, but each has its own instance of that hook's associated state.

觉得这看起来有点奇怪吗?你不是一个人!

¥Thinking this looks a little strange? You're not alone!

我们中的许多人花了一段时间才习惯这种方法。

¥It took many of us a while to grow accustomed to this approach.

依赖参数

¥The dependency argument

许多钩子接受一个可用于限制何时更新钩子的参数。Preact 检查依赖数组中的每个值,并检查自上次调用钩子以来它是否已更改。当未指定依赖参数时,钩子总是被执行。

¥Many hooks accept an argument that can be used to limit when a hook should be updated. Preact inspects each value in a dependency array and checks to see if it has changed since the last time a hook was called. When the dependency argument is not specified, the hook is always executed.

在上面的 useCounter() 实现中,我们将一系列依赖传递给 useCallback()

¥In our useCounter() implementation above, we passed an array of dependencies to useCallback():

function useCounter() {
  const [value, setValue] = useState(0);
  const increment = useCallback(() => {
    setValue(value + 1);
  }, [value]);  // <-- the dependency array
  return { value, increment };
}

每当 value 更改时,在此处传递 value 都会导致 useCallback 返回新的函数引用。为了避免 "旧的闭包",这是必要的,其中回调将始终引用第一个渲染创建时的 value 变量,导致 increment 始终设置 1 的值。

¥Passing value here causes useCallback to return a new function reference whenever value changes. This is necessary in order to avoid "stale closures", where the callback would always reference the first render's value variable from when it was created, causing increment to always set a value of 1.

每次 value 更改时,这都会创建一个新的 increment 回调。出于性能原因,通常最好使用 callback 来更新状态值,而不是使用依赖保留当前值。

¥This creates a new increment callback every time value changes. For performance reasons, it's often better to use a callback to update state values rather than retaining the current value using dependencies.

有状态的钩子

¥Stateful hooks

在这里,我们将了解如何将状态逻辑引入到功能组件中。

¥Here we'll see how we can introduce stateful logic into functional components.

在引入钩子之前,任何需要状态的地方都需要类组件。

¥Prior to the introduction of hooks, class components were required anywhere state was needed.

useState

该钩子接受一个参数,这将是初始状态。调用时,该钩子返回一个包含两个变量的数组。第一个是当前状态,第二个是我们状态的设置者。

¥This hook accepts an argument, this will be the initial state. When invoked this hook returns an array of two variables. The first being the current state and the second being the setter for our state.

我们的 setter 的行为与经典状态的 setter 类似。它接受一个以 currentState 作为参数的值或函数。

¥Our setter behaves similar to the setter of our classic state. It accepts a value or a function with the currentState as argument.

当你调用 setter 并且状态不同时,它将触发从使用 useState 的组件开始重新渲染。

¥When you call the setter and the state is different, it will trigger a rerender starting from the component where that useState has been used.

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>
  )
}
Run in REPL

当我们的初始状态很昂贵时,最好传递一个函数而不是一个值。

¥When our initial state is expensive it's better to pass a function instead of a value.

useReducer

useReducer 钩子与 redux 非常相似。与 useState 相比,当你具有复杂的状态逻辑(其中下一个状态取决于前一个状态)时,它更容易使用。

¥The useReducer hook has a close resemblance to redux. Compared to useState it's easier to use when you have complex state logic where the next state depends on the previous one.

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>
  );
}
Run in REPL

记忆化

¥Memoization

在 UI 编程中,通常存在一些计算成本较高的状态或结果。记忆化可以缓存该计算的结果,以便在使用相同的输入时可以重复使用它。

¥In UI programming there is often some state or result that's expensive to calculate. Memoization can cache the results of that calculation allowing it to be reused when the same input is used.

useMemo

使用 useMemo 钩子,我们可以记住该计算的结果,并且仅在依赖之一发生变化时才重新计算它。

¥With the useMemo hook we can memoize the results of that computation and only recalculate it when one of the dependencies changes.

const memoized = useMemo(
  () => expensive(a, b),
  // Only re-run the expensive function when any of these
  // dependencies change
  [a, b]
);

不要在 useMemo 内运行任何有效的代码。副作用属于 useEffect

¥Don't run any effectful code inside useMemo. Side-effects belong in useEffect.

useCallback

useCallback 钩子可用于确保只要依赖没有更改,返回的函数就保持引用相等。当子组件依赖引用相等来跳过更新(例如 shouldComponentUpdate)时,这可用于优化子组件的更新。

¥The useCallback hook can be used to ensure that the returned function will remain referentially equal for as long as no dependencies have changed. This can be used to optimize updates of child components when they rely on referential equality to skip updates (e.g. shouldComponentUpdate).

const onClick = useCallback(
  () => console.log(a, b),
  [a, b]
);

有趣的事实:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

¥Fun fact: useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

useRef

要获取对功能组件内的 DOM 节点的引用,可以使用 useRef 钩子。它的工作原理与 createRef 类似。

¥To get a reference to a DOM node inside a functional components there is the useRef hook. It works similar to 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>
    </>
  );
}
Run in REPL

注意不要将 useRefcreateRef 混淆。

¥Be careful not to confuse useRef with createRef.

useContext

要访问功能组件中的上下文,我们可以使用 useContext 钩子,而不需要任何高阶或封装组件。第一个参数必须是从 createContext 调用创建的上下文对象。

¥To access context in a functional component we can use the useContext hook, without any higher-order or wrapper components. The first argument must be the context object that's created from a createContext call.

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>
  )
}
Run in REPL

副作用

¥Side-Effects

副作用是许多现代应用的核心。无论你是想从 API 获取一些数据还是触发文档效果,你都会发现 useEffect 几乎可以满足你的所有需求。这是 hooks API 的主要优点之一,它重塑了你的思维,让你思考效果而不是组件的生命周期。

¥Side-Effects are at the heart of many modern Apps. Whether you want to fetch some data from an API or trigger an effect on the document, you'll find that the useEffect fits nearly all your needs. It's one of the main advantages of the hooks API, that it reshapes your mind into thinking in effects instead of a component's lifecycle.

useEffect

顾名思义,useEffect 是触发各种副作用的主要方式。如果需要的话,你甚至可以从效果中返回一个清理函数。

¥As the name implies, useEffect is the main way to trigger various side-effects. You can even return a cleanup function from your effect if one is needed.

useEffect(() => {
  // Trigger your effect
  return () => {
    // Optional: Any cleanup code
  };
}, []);

我们将从 Title 组件开始,该组件应反映文档的标题,以便我们可以在浏览器选项卡的地址栏中看到它。

¥We'll start with a Title component which should reflect the title to the document, so that we can see it in the address bar of our tab in our browser.

function PageTitle(props) {
  useEffect(() => {
    document.title = props.title;
  }, [props.title]);

  return <h1>{props.title}</h1>;
}

useEffect 的第一个参数是触发效果的无参数回调。在我们的例子中,我们只想在标题确实发生变化时触发它。当它保持不变时,就没有必要更新它。这就是为什么我们使用第二个参数来指定 dependency-array

¥The first argument to useEffect is an argument-less callback that triggers the effect. In our case we only want to trigger it, when the title really has changed. There'd be no point in updating it when it stayed the same. That's why we're using the second argument to specify our dependency-array.

但有时我们有更复杂的用例。想象一个组件,它在安装时需要订阅一些数据,在卸载时需要取消订阅。这也可以通过 useEffect 来完成。要运行任何清理代码,我们只需要在回调中返回一个函数。

¥But sometimes we have a more complex use case. Think of a component which needs to subscribe to some data when it mounts and needs to unsubscribe when it unmounts. This can be accomplished with useEffect too. To run any cleanup code we just need to return a function in our callback.

// 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>;
}
Run in REPL

清理功能是可选的。如果你不需要运行任何清理代码,则无需在传递给 useEffect 的回调中返回任何内容。

¥The cleanup function is optional. If you don't need to run any cleanup code, you don't need to return anything in the callback that's passed to useEffect.

useLayoutEffect

该签名与 useEffect 相同,但一旦组件发生差异并且浏览器有机会绘制,它就会触发。

¥The signature is identical to useEffect, but it will fire as soon as the component is diffed and the browser has a chance to paint.

useErrorBoundary

每当子组件抛出错误时,你都可以使用此钩子来捕获它并向用户显示自定义错误 UI。

¥Whenever a child component throws an error you can use this hook to catch it and display a custom error UI to the user.

// 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

¥For monitoring purposes it's often incredibly useful to notify a service of any errors. For that we can leverage an optional callback and pass that as the first argument to useErrorBoundary.

const [error] = useErrorBoundary(error => callMyApi(error.message));

完整的用法示例可能如下所示:

¥A full usage example may look like this:

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 中引入的。

¥If you've been using the class based component API in the past, then this hook is essentially an alternative to the componentDidCatch lifecycle method. This hook was introduced with Preact 10.2.0.

实用钩子

¥Utility hooks

useId

这个钩子将为每次调用生成一个唯一的标识符,并保证在渲染 在服务器上 和客户端时这些标识符是一致的。一致 ID 的一个常见用例是表单,其中 <label> 元素使用 for 属性将它们与特定的 <input> 元素关联起来。useId 钩子不仅仅与表单绑定,并且可以在你需要唯一 ID 时使用。

¥This hook will generate a unique identifier for each invocation and guarantees that these will be consistent when rendering both on the server and the client. A common use case for consistent IDs are forms, where <label>-elements use the for attribute to associate them with a specific <input>-element. The useId hook isn't tied to just forms though and can be used whenever you need a unique ID.

为了使钩子保持一致,你需要在服务器和客户端上使用 Preact。

¥To make the hook consistent you will need to use Preact on both the server as well as on the client.

完整的用法示例可能如下所示:

¥A full usage example may look like this:

const App = props => {
  const mainId = useId();
  const inputId = useId();

  useLayoutEffect(() => {
    document.getElementById(inputId).focus()
  }, [])

  // Display an input with a unique ID.
  return (
    <main id={mainId}>
      <input id={inputId}>
    </main>
  )
};

该钩子是在 Preact 10.11.0 中引入的,需要 preact-render-to-string 5.2.4。

¥This hook was introduced with Preact 10.11.0 and needs preact-render-to-string 5.2.4.

Preact 中文网 - 粤ICP备13048890号