简介
假设我们正在建立一个日历应用程序,像谷歌日历一样。该应用允许你在三种不同的显示方式之间切换:月、周和日。
在一个典型的日历应用程序中切换视图
就个人而言,我总是希望看到 "周 "视图。它为我提供了我需要知道的关于当前一天的所有信息,同时也让我窥见了未来几天的内容。*
值得庆幸的是,日历应用程序知道用户对这种事情有很强的偏好,而且这种切换是*"粘性的"。*如果我从 "周 "切换到 "月",然后刷新页面,"月 "视图就会成为新的默认视图;它会一直存在。
相反,如果表单控件不是粘性的,那就超级烦人了。例如:每个月我都会通过Expensify创建4-5项支出。每一次,我都要把默认货币从美元换成加元。为什么它不记得我是加拿大人呢?
在本教程中,我们将看到如何创建一个自定义的React钩子来抽象出 "粘性",这样我们就可以在需要的时候免费得到它。
给我看看代码
下面是我们的自定义钩子的样子。
function useStickyState(defaultValue, key) {
const [value, setValue] = React.useState(() => {
const stickyValue = window.localStorage.getItem(key);
return stickyValue !== null
? JSON.parse(stickyValue)
: defaultValue;
});
React.useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
那SSR呢?
如果你的应用程序是服务器渲染的(使用Next.js或Gatsby这样的框架),如果你试图按原样使用这个钩子,你会得到一个错误。
这实际上是一个相当棘手的问题,因为服务器上的第一次渲染并不能访问你的计算机的localStorage;它不可能知道初始值应该是什么。
为了显示它是如何工作的,这里有一个快速的计数器演示,有一个粘性计数。试着点击它几次,然后刷新这个页面。
function App() {
const [
count,
setCount
] = useStickyState(0, "count");
return (
<div className="App">
<h1>Counter</h1>
<p>Current count: {count}</p>
<button
onClick={() => setCount(count + 1)}
>
Increment
</button>
</div>
);
}
render(<App />);
如果这段代码对你来说还不清楚,不要害怕!本教程的其余部分将解释它。本教程的其余部分会更详细地解释它 💫
在实践中
这个钩子做了一个假设,这在React应用程序中是相当安全的:支持表单输入的值被保存在React状态中。
这里有一个表单控件的非粘性实现,可以在不同的值之间切换。
const CalendarView = () => {
const [mode, setMode] = React.useState('day');
return (
<>
<select onChange={ev => setMode(ev.target.value)}>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
</select>
{/* Calendar stuff here */}
</>
)
}
We can use our ne
我们可以通过换掉钩子来使用我们新的 "粘性 "变体。
const CalendarView = () => {
const [mode, setMode] = useStickyState('day', 'calendar-view');
// Everything else unchanged
}
虽然useState 钩子只需要1个参数--初始值,但我们的useStickyState 钩子需要两个参数。第二个参数是用来获取和设置持久化在localStorage中的值的键。你给它的标签必须是唯一的,否则它是什么并不重要。
它是如何工作的
从根本上说,这个钩子是一个围绕useState 的包装器。它也只是做一些其他的事情。
懒惰的初始化
首先,它利用了懒惰初始化的优势。这让我们可以传递一个函数给useState ,而不是一个值,而且这个函数只会在组件第一次渲染时执行,当状态被创建时。
const [value, setValue] = React.useState(() => {
const stickyValue =
window.localStorage.getItem(key);
return stickyValue !== null
? JSON.parse(stickyValue)
: defaultValue;
});
在我们的例子中,我们用它来检查 localStorage 中的值。如果该值存在,我们将使用它作为我们的初始值。否则,我们将使用传递给钩子的默认值("day",在我们前面的例子中)。
保持localStorage的同步性
这方面的最后一步是确保每当状态值发生变化时我们都要更新localStorage。为此,我们可信赖的朋友useEffect ,就派上用场了。
React.useEffect(() => {
window.localStorage.setItem(name, JSON.stringify(value));
}, [name, value]);
快速更新?
如果状态值快速变化(比如,一秒钟内多次),你可能希望对localStorage的更新进行节流或减震。因为localStorage是一个同步的API,如果更新的速度过快,会导致性能问题。
但不要以此为借口过早地进行优化。剖析器会告诉你是否需要对你的更新进行节流。
包裹起来
这个钩子是一个小而强大的例子,说明自定义钩子如何让我们为事物发明自己的API。虽然存在为我们解决这个问题的包,但我认为看到如何自己解决这些问题有很大的价值 🧙🏻♂️