Next.JS - 在渲染页面之前访问 `localStorage`

IT技术 node.js reactjs next.js
2021-05-06 13:20:18

假设我有一个用户的帐户信息存储在localStorage(客户端)中。我需要我的 Next.JS 应用程序根据存储在localStorage(登录或注销按钮)中的内容来呈现网页的导航栏如何首先从客户端获取值然后呈现页面?或者这甚至不是 Next.JS 的意图?

4个回答

你可以这样做:

  1. 在状态中使用一个变量来防止页面被渲染
  2. 用于componentDidMount从中加载数据localStorage
  3. 加载数据时,设置状态以允许呈现组件。

这是一个react问题,而不是 next.js 问题。您可以在第 1 步中使用条件渲染。还可以在此处阅读状态,最后是componentDidMount

更新:

现在,我会选择 React hooks 实现,但这个想法仍然存在。useEffect在某些情况下,可以通过一些细微差别在很大程度上实现这一点。

我也意识到 NextJS 和 SSR 逻辑有一些可能的警告,所以这个响应可能不够。在这种情况下,我还会查看下面的其他一些回复。

正如在https://stackoverflow.com/a/54819843/895245 中提到的,localStorage在第一次渲染之前我无法真正获得,只显示后备页面直到发生这种情况。

根本问题是 Next.js 将一个 URL 映射到一个预渲染。而 React hydration需要初始服务器 HTML 来匹配 JavaScript 结构

React 期望渲染的内容在服务器和客户端之间是相同的。它可以修补文本内容的差异,但您应该将不匹配视为错误并修复它们。在开发模式下,React 会在水合期间警告不匹配。不能保证在不匹配的情况下修补属性差异。出于性能原因,这很重要,因为在大多数应用程序中,不匹配的情况很少见,因此验证所有标记的成本高得令人望而却步。

该引用不是很清楚纯文本更改是否有效,但下面的最小测试表明它在这种情况下会发出警告,因此您不想使用它:

因此,它是useEffect之后用于更新页面的唯一可靠方式

但是,当我测试时,正确的渲染localStorage很快就会显示出来,中间的渲染根本不明显,我不确定它是否会发生。

我想做的是对那个答案中提到的内容给出一个更具体的想法。

驻波比示例

这是一个完整的可运行示例,其中导航栏显示登录状态:https://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app该存储库是this one 的一个分支,两者都是 Next真棒真实世界项目的.js 实现

在这种情况下,回退只是博客页面的注销视图,其中已经包含用户可能想要查看的关键信息,并且可以缓存,例如使用 ISR。

该演示使用SWR使代码稍微简单一些。关键部分是:

代码的关键部分是:

导航栏:

import useSWR from "swr";

const Navbar = () => {
  const { data: currentUser } = useSWR("user", key => {
    const value = localStorage.getItem(key);
    return !!value ? JSON.parse(value) : undefined;
  });

登录:

import { mutate } from "swr";

const LoginForm = () => {
  const handleSubmit = async (e) => {
    // Get `user` data structure from API.
    mutate("user", data?.user);

我们看到,当用户登录时,我们调用mutate"user"全局标识符。

这将重绘包含该钩子的所有组件,其中包括导航栏,因为它通过useSWR调用设置了钩子

这样,login首先重绘导航栏,然后重定向到首页,这样首页就会立即有重绘的导航栏。没有 mutate,只有页面主体会重绘,而不是导航栏。

使用此设置:

  • 如果你把 a 放在console.log(currentUser)下面useSWR,你会看到它被调用了两次。

    所以会发生什么是它首先立即返回一个缓存值 ( undefined) 并开始第一个渲染。

    然后它启动对缓存的异步调用,当它返回时,钩子触发组件的重新渲染,并使用当前用户值再次打印。

    这仅发生在刷新/第一次命中期间的初始水合时。在内部页面更改期间,只有一个渲染。

    所有这一切发生得如此之快,以至于我根本看不到没有本地存储的页面绘制,即使使用 Chromium 调试器时间线帧检查也看不到。

  • 但是,如果我们向localStoragegetter添加 2 秒延迟

    const { data: currentUser } = useSWR("user", async (key) => {
      await new Promise(resolve => setTimeout(resolve, 2000))
      const value = localStorage.getItem(key);
      return !!value ? JSON.parse(value) : undefined;
    });
    

    我们确实观察到用户注销的中间页面状态,因此理论上可能会发生。

没有 SWR 会是什么样子

当然,我们不需要使用 SWR 来实现这一点。

SWR 文档为我们提供了没有 SWR https://swr.vercel.app/getting-started来激励他们的库的情况的基本原理

您需要将状态向上移动到登录表单 + 导航栏的公共父级,或者您可以使用Context

function Page () {
  const [user, setUser] = useState(null)

  // fetch data
  useEffect(() => {
    const value = localStorage.getItem(key);
    const user = !!value ? JSON.parse(value) : undefined;
    setUser(user)
  }, [])

  // global loading state
  if (!user) return <Spinner/>

  return <div>
    <Navbar user={user} />
    <Content user={user} />
  </div>
}

// child components

function Navbar ({ user }) {
  return <div>
    ...
    <Avatar user={user} />
  </div>
}

function Content ({ user }) {
  return <h1>Welcome back, {user.name}</h1>
}

function Avatar ({ user }) {
  return <img src={user.avatar} alt={user.name} />
}

正如生命周期方法和 useEffect 钩子有什么区别? useEffect是类似于 的钩子componentDidMount

检查typeof localStorage === 'undefined'导致警告

React 不喜欢这样,并警告如下:

预期的服务器 HTML 包含匹配的"

因为它注意到了水合页面和非水合页面之间的区别:React 16: Warning: E​​xpected server HTML to contains a matching <div> in <body>

在 Next.js 10.2.2 上测试。

最小的可重复示例

只是为了玩,看看会发生什么:

页面/index.js

import Link from 'next/link'
import React from 'react'

export default function IndexPage() {
  console.error('IndexPage');
  let [n, setN] = React.useState(0)
  if (typeof localStorage === 'undefined') {
    n = '0'
  } else {
    n = parseInt(localStorage.getItem('n') || '0', 10)
  }
  return <>
    <Link href="/notindex">notindex</Link>
    <div
      onClick={() => {
        localStorage.setItem('n', n + 1)
        setN(n + 1)
      }}
    >increment</div>
    <div
      onClick={() => {
        localStorage.removeItem('n')
        setN(0)
      }}
    >reset</div>
    <div>{n}</div>
  </>
}

页面/notindex.js

import Link from 'next/link'

export default function NotIndexPage() {
  return <Link href="/">index</Link>
}

包.json

{
  "name": "test",
  "version": "1.0.0",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "12.0.7",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  }
}

跑:

npm install
npm run dev

现在,如果你:

  • 打开 /
  • 增量
  • 刷新页面

react 发出警告,因为它注意到0文本已更改为1

警告:文本内容不匹配。服务器:“0” 客户端:“1”

但是,如果我们单击内部链接notindex来回,则不会看到警告。这是因为 hydration 仅在初始页面刷新时完成,进一步的更改仅在 Js 中完成。

我们必须做的是这样的:

import Link from 'next/link'
import React from 'react'

export default function IndexPage() {
  console.error('IndexPage');
  let [n, setN] = React.useState(0)
  React.useEffect(() => {
    console.error('useEffect');
    setN(parseInt(localStorage.getItem('n') || '0', 10))
  }, [])
  return <>
    <Link href="/notindex">notindex</Link>
    <div
      onClick={() => {
        setN(n + 1)
        localStorage.setItem('n', n + 1)
      }}
    >increment</div>
    <div
      onClick={() => {
        localStorage.removeItem('n')
        setN(0)
      }}
    >reset</div>
    <div>{n}</div>
  </>
}

我就是这样做的。

const setSession = (accessToken) => {
   if (typeof window !== 'undefined')
     localStorage.setItem('accessToken', accessToken);
};

const getAccessToken = () => {
   if (typeof window !== 'undefined') 
      return localStorage.getItem('accessToken');
};

这是我调用它们来处理登录和获取访问令牌的地方:

const loginWithEmailAndPassword = async (email, password) => {
  const { data } = await axios.post(`${apiUrl}/login`, { email, password });
  const { user, accessToken } = data;

  if (user) {
    setSession(accessToken);
    return user;
  }
};

const accessToken = getAccessToken();

本地存储在服务器上不可用,有两个选项可以解决这个问题 1:创建 HOC 或自定义钩子来检查本地存储是否有数据(这是正常的react方式) 2:您可以使用 cookie 在客户端存储数据和服务器端,然后可以使用 getServerSideProps 来提取数据,然后您可以使用这些数据在初始渲染中相应地显示信息。