假设我有一个用户的帐户信息存储在 localStorage
(客户端)。我需要我的 Next.JS 应用程序根据存储在 localStorage
(登录或注销按钮)中的内容来呈现网页的导航栏。如何先从客户端获取值, 然后 渲染页面?或者也许这甚至不是 Next.JS 的本意?
原文由 APixel Visuals 发布,翻译遵循 CC BY-SA 4.0 许可协议
假设我有一个用户的帐户信息存储在 localStorage
(客户端)。我需要我的 Next.JS 应用程序根据存储在 localStorage
(登录或注销按钮)中的内容来呈现网页的导航栏。如何先从客户端获取值, 然后 渲染页面?或者也许这甚至不是 Next.JS 的本意?
原文由 APixel Visuals 发布,翻译遵循 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 页面/notindex.js 包.json 跑步: 现在,如果你: React 发出警告,因为它注意到 警告:文本内容不匹配。服务器:“0” 客户端:“1” 但是,如果我们单击指向 我们要做的是这样的: 区分“未登录”和“尚未决定”以避免进行额外的 API 调用 好的,我遇到了另一个问题:我进行了不必要的 API 调用,因为首先页面认为用户已注销,然后它认为它已登录,并且每个都需要进行不同的 API 调用。 与开始呈现错误页面不同,这实际上会导致服务器负载后果,因此这是不可接受的。 我使用的解决方案是区分: 并且不对 undefined 提出任何请求。 这是一个非最小化的演示: 我稍后会尽量减少它。 另一种解决方案:只做SSR 一般来说,SSR 比 ISR 简单得多,因为你不必担心这个获取页面/请求数据/获取数据/更新页面的地狱之舞。 ISR 是一种优化,只有在性能优势得到证实的情况下才应使用。 请记住,Next.js 中的 SSR 的数据效率也很高,因为 Next.js 在页面切换时仅返回 然后,您可以使用 cookie 从 原文由 Ciro Santilli OurBigBook.com 发布,翻译遵循 CC BY-SA 4.0 许可协议 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>
</>
}
import Link from 'next/link'
export default function NotIndexPage() {
return <Link href="/">index</Link>
}
{
"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
/
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>
</>
}
.json
来自 getServerSideProps
,基本上与 API 完全一样。getServerSideProps
进行身份验证,并立即返回正确的页面。
5 回答4.8k 阅读✓ 已解决
4 回答2.5k 阅读✓ 已解决
2 回答1.7k 阅读✓ 已解决
5 回答1.9k 阅读
2 回答1.3k 阅读✓ 已解决
3 回答2k 阅读
1 回答3.2k 阅读
你可以这样做:
componentDidMount
从localStorage
加载数据这是一个反应问题,而不是 next.js 问题。您可以在步骤 1 中使用 条件渲染。还 可以在此处阅读状态,最后
componentDidMount
。更新:
现在,我会选择 React hooks 实现,但这个想法仍然存在。
useEffect
在某些情况下可以通过一些细微差别在很大程度上实现这一点。我也意识到 NextJS 和 SSR 逻辑有一些可能的警告,所以这个响应可能还不够。在这种情况下,我还会研究下面的一些其他回复。