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

新手上路,请多包涵

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

原文由 APixel Visuals 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 2.6k
2 个回答

你可以这样做:

  1. 在状态中使用变量来防止页面被渲染
  2. 使用 componentDidMountlocalStorage 加载数据
  3. 加载数据时,设置状态以允许渲染组件。

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

更新:

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

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

原文由 Keno 发布,翻译遵循 CC BY-SA 4.0 许可协议

https://stackoverflow.com/a/54819843/895245 所述,在第一次呈现之前,我无法真正获得 localStorage ,仅显示后备页面,直到发生这种情况。

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

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

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

因此,唯一可靠的方法是使用 useEffect 之后更新页面。

然而,当我测试时,正确的渲染 localStorage 显示得如此之快以至于中间的渲染根本不明显,我不确定它是否会发生。唯一的问题是,如果您对每种情况进行不同的 API 调用,请参阅下面的“区分“未登录”和“尚未决定”以避免进行额外的 API 调用”部分的示例。

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

驻波比示例

这是一个完整的可运行示例,其中导航栏显示登录状态: https ://github.com/cirosantilli/node-express-sequelize-nextjs-realworld-example-app 该存储库是 这个 存储库的一个分支,两者都是 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);

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

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

这样login先重绘navbar,然后再跳转到home,这样首页马上就有重绘的navbar了。如果没有 mutate,只有页面主体会重绘,而不是导航栏。

使用此设置:

  • 如果您将 --- 放在 --- useSWR console.log(currentUser) 下方,您会看到它被调用了两次。

所以发生的事情是它首先立即返回一个缓存值( undefined )并且第一次渲染开始。

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

这仅发生在刷新/首次点击期间的初始水合作用。在内部页面更改期间,只有一个渲染。

所有这一切发生得如此之快,以至于我根本无法在没有 hte 本地存储的情况下看到页面绘制,即使使用 Chromium 调试器时间轴框架检查也看不到。

  • 但是,如果我们向 localStorage 吸气剂添加 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:警告:期望服务器 HTML 在 中包含匹配的

在 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 的内部链接并返回,我们不会看到警告。这是因为水化仅在初始页面刷新时完成,进一步的更改仅在 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>
  </>
}

区分“未登录”和“尚未决定”以避免进行额外的 API 调用

好的,我遇到了另一个问题:我进行了不必要的 API 调用,因为首先页面认为用户已注销,然后它认为它已登录,并且每个都需要进行不同的 API 调用。

与开始呈现错误页面不同,这实际上会导致服务器负载后果,因此这是不可接受的。

我使用的解决方案是区分:

  • undefined: 还没有决定
  • null:未登录

并且不对 undefined 提出任何请求。

这是一个非最小化的演示:

我稍后会尽量减少它。

另一种解决方案:只做SSR

一般来说,SSR 比 ISR 简单得多,因为你不必担心这个获取页面/请求数据/获取数据/更新页面的地狱之舞。

ISR 是一种优化,只有在性能优势得到证实的情况下才应使用。

请记住,Next.js 中的 SSR 的数据效率也很高,因为 Next.js 在页面切换时仅返回 .json 来自 getServerSideProps ,基本上与 API 完全一样。

然后,您可以使用 cookie 从 getServerSideProps 进行身份验证,并立即返回正确的页面。

原文由 Ciro Santilli OurBigBook.com 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题