如何将React状态持久化在localStorage中

287 阅读4分钟

简介

假设我们正在建立一个日历应用程序,像谷歌日历一样。该应用允许你在三种不同的显示方式之间切换:月、周和日。

在一个典型的日历应用程序中切换视图

就个人而言,我总是希望看到 "周 "视图。它为我提供了我需要知道的关于当前一天的所有信息,同时也让我窥见了未来几天的内容。*

值得庆幸的是,日历应用程序知道用户对这种事情有很强的偏好,而且这种切换是*"粘性的"。*如果我从 "周 "切换到 "月",然后刷新页面,"月 "视图就会成为新的默认视图;它会一直存在。

相反,如果表单控件不是粘性的,那就超级烦人了。例如:每个月我都会通过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。虽然存在为我们解决这个问题的,但我认为看到如何自己解决这些问题有很大的价值 🧙🏻♂️