在 React 中创建自定义钩子(hooks)是一个非常有价值的练习,甚至可以说是许多开发者的“啊哈”时刻。
剧透警告:第一次尝试是在 Revolut 的一次高压面试中,结果并不如意。
在这篇文章中,带你一起回顾我重现著名的 useState() 钩子的历程,称它为 useMyState()。同时,我也会给出一些建议,帮助你下次面试时避免失败。
让我们一起来探讨哪里出了问题,我是如何重新拾起信心的,以及你如何避免犯同样的错误。准备好迎接一些高级 React 的乐趣吧!🎉
梦寐以求的工作和艰难的面试
拿到 Revolut 的面试机会感觉就像中了彩票。我满怀激动和信心,因为我花了数周时间打磨我的 React 和 TypeScript 技能。
我以为我已经掌握了一切——但面试让我意识到,我的信心过于自满。这个面试,我既兴奋又紧张,然而很快,这次经历变成了一次让我永生难忘的挫败。
在顺利通过一些初步面试轮次后,我遇到了一个我没有完全预料到的挑战:“你能从头开始重现 useState() 钩子吗?”
这个问题看似简单,但其背后的意义深远。我知道 useState() 在高层次上的工作原理,但在面试现场,面对时钟滴答作响,要从头构建它却是另一回事。
任务:在压力下重现 useState()
面试官解释说,我需要创建一个自定义钩子,模仿 React 的 useState() 的行为。
这个钩子应该存储状态,允许状态更新,并在状态变化时触发重新渲染。
听起来很熟悉,对吧?我曾经读过这类内容,甚至考虑过如何实现。但现在,在面试压力下,一切似乎变得更加复杂。
关键问题是:我从未实际在现场实现过类似的东西。
概念在我脑海中很清晰,但在面对一个可能决定我职业生涯的人时,执行起来却完全不同。
最终,我没有完成这个任务……但后来我决定在没有时间压力的情况下再试一次,结果是这样的。
为什么要重现 useState()?
你可能会问,“为什么要重现一个已经完美运作的东西?”嗯,这正是我为什么要做的原因——因为它确实运作得很好。
理解 useState() 的底层工作原理有助于你更好地掌握 React 的“魔法”。
此外,构建你自己的工具——即使它们已经存在——也是一种非常有成就感的体验。
入门:useMyState() 背后的概念
在开始编码之前,让我们明确我们想要实现的目标。React 中的 useState() 钩子:
- 返回一个状态变量和一个用于更新该状态的函数。
- 每当状态更新时,重新渲染组件。
我们的 useMyState() 钩子应该实现相同的功能。本质上,我们需要一种方式来存储状态,更新它,并在发生变化时触发重新渲染。听起来很简单,对吧?嗯,这既简单又复杂。
构建模块:useState() 的底层工作原理
首先,让我们讨论一下 React 如何在内部管理状态。当你调用 useState() 时,React 在内部通过一个称为“钩子状态队列”的机制来跟踪你的状态。
每当你调用更新函数(我们称之为 setState)时,React 并不会立即更新状态。相反,它会调度一次重新渲染。
在这次重新渲染期间,React 根据队列计算新的状态。
要复制这种行为,我们需要:
- 将状态存储在一个可以跨渲染持续存在的地方。
- 提供一个函数来更新这个状态并触发重新渲染。
创建 useMyState():编写一些代码!
我们可以这样开始。首先,我们需要创建一个组件级别的状态存储。
由于 React 并不提供一种直接的方式来在渲染之间保持变量的持久性(除了使用类似 useState() 或 useRef() 的钩子),我们需要使用 useRef() 来实现这一点。
function useMyState(initialValue) {
const stateRef = React.useRef(initialValue);
const [, forceRender] = React.useReducer(x => x + 1, 0);
const setState = (newValue) => {
stateRef.current = newValue;
forceRender();
};
return [stateRef.current, setState];
}
细分解释
- useRef(initialValue): 这是我们的状态所在。useRef() 可以在渲染之间保持值,而不会导致重新渲染。
- useReducer(x => x + 1, 0): 这是一个巧妙的技巧,用来强制组件重新渲染。我们不关心 x 的值,只是调用 forceRender() 会使 React 重新渲染我们的组件。
- setState(newValue): 我们更新 stateRef 的 current 属性,然后触发一次重新渲染。
就这样!通过这三部分,我们就有了自己的 useState() 版本。但我们怎么知道它是否有效呢?让我们测试一下。
测试 useMyState(): 让它工作
让我们看看这个钩子的实际效果。这里有一个简单的计数器组件,使用我们的 useMyState():
function Counter() {
const [count, setCount] = useMyState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
当你点击“Increment”按钮时,计数器应该增加,组件会重新渲染以反映新的计数。这就像使用 useState() 一样。很棒吧?
处理多个状态:扩展 useMyState()
现在,如果我们需要在同一个组件中管理多个状态会怎么样?React 的 useState() 可以优雅地处理这个问题,我们的 useMyState() 也可以。
function MultipleStatesComponent() {
const [count, setCount] = useMyState(0);
const [text, setText] = useMyState('Hello');
return (
<div>
<p>{count}</p>
<p>{text}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setText('World')}>Change Text</button>
</div>
);
}
这个组件管理两个独立的状态变量,useMyState() 像 useState() 一样处理它们。
总结
React 的 useState() 钩子经过高度优化、经过实战检验,能够处理我们简单的 useMyState() 可能忽略的边缘情况。
尽管我未能在面试中重现它,但我能够在一个平静的环境中自己重新实现它,下次面试时,我会准备得更好。
重现这样的工具并不是为了取代它们,而是为了学习它们的工作原理,从而更好地使用它们。
谁知道呢?你也许会在一个副项目中找到 useMyState() 的用武之地,或者只是把它当作一个有趣的技巧留在你的工具箱里。
祝你好运!