挂钩
Hooks API 是一种新概念,它允许您组合状态和副作用。Hooks 允许您在组件之间重用有状态逻辑。
如果您已经使用 Preact 一段时间了,您可能熟悉“渲染道具”和“高阶组件”等模式,它们试图解决这些挑战。这些解决方案往往使代码更难遵循且更抽象。hooks API 使得可以巧妙地提取状态和副作用的逻辑,还可以独立于依赖它的组件简化单元测试该逻辑。
Hooks 可以在任何组件中使用,并且避免了类组件 API 所依赖的this
关键字的许多缺陷。hooks 依赖闭包,而不是从组件实例访问属性。这使得它们具有值绑定,并消除了在处理异步状态更新时可能发生的许多陈旧数据问题。
有两种方法可以导入 hooks:从preact/hooks
或preact/compat
。
简介
了解 hooks 的最简单方法是将它们与等效的基于类的组件进行比较。
我们将使用一个简单的计数器组件作为我们的示例,它渲染一个数字和一个按钮,该按钮将数字增加一
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 版本。
让我们将计数器逻辑提取到一个自定义钩子中,使其在组件之间轻松重复使用
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()
自定义钩子,但每个钩子都有自己关联状态的实例。
觉得这看起来有点奇怪?你并不孤单!
我们许多人花了一段时间才习惯这种方法。
依赖项参数
许多钩子接受一个参数,该参数可用于限制钩子应更新的时间。Preact 检查依赖项数组中的每个值,并检查自上次调用钩子以来它是否已更改。当未指定依赖项参数时,始终执行钩子。
在上面的 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
回调。出于性能原因,通常最好使用 回调 来更新状态值,而不是使用依赖项保留当前值。
有状态钩子
在这里,我们将看到如何将有状态逻辑引入函数组件。
在引入钩子之前,任何需要状态的地方都需要类组件。
useState
此钩子接受一个参数,这将是初始状态。调用此钩子时,它将返回一个包含两个变量的数组。第一个是当前状态,第二个是状态的设置器。
我们的设置器与经典状态的设置器类似。它接受一个值或一个以 currentState 为参数的函数。
当您调用设置器并且状态不同时,它将从使用该 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 相比,当您具有复杂的 state 逻辑(其中下一个 state 取决于前一个 state)时,它更容易使用。
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。