假设我有一个用户的帐户信息存储在localStorage(客户端)中。我需要我的 Next.JS 应用程序根据存储在localStorage(登录或注销按钮)中的内容来呈现网页的导航栏。如何首先从客户端获取值然后呈现页面?或者这甚至不是 Next.JS 的意图?
Next.JS - 在渲染页面之前访问 `localStorage`
你可以这样做:
- 在状态中使用一个变量来防止页面被渲染
- 用于
componentDidMount从中加载数据localStorage - 加载数据时,设置状态以允许呈现组件。
这是一个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: Expected 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 来提取数据,然后您可以使用这些数据在初始渲染中相应地显示信息。