为什么下面的 HOC 会不断刷新,即使它返回的是一个记忆化的组件?
Why does the following HOC keep refreshing even though it is returning a memoized component?
对于上下文,我正在将 Django Rest Framework 后端与 Next.js + Next-Auth 集成。除了一部分,我已经完成了大部分集成。要求是有一个刷新令牌系统,该系统将在访问令牌快过期时尝试刷新访问令牌。这是我的逻辑:
/api/auth/[...nextauth].ts
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { NextAuthOptions } from "next-auth";
import Providers from "next-auth/providers";
import axios from "axios";
import { AuthenticatedUser } from "../../../types";
import { JwtUtils, UrlUtils } from "../../../constants/Utils";
namespace NextAuthUtils {
export const refreshToken = async function (refreshToken) {
try {
const response = await axios.post(
// "http://localhost:8000/api/auth/token/refresh/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"auth",
"token",
"refresh",
),
{
refresh: refreshToken,
},
);
const { access, refresh } = response.data;
// still within this block, return true
return [access, refresh];
} catch {
return [null, null];
}
};
}
const settings: NextAuthOptions = {
secret: process.env.SESSION_SECRET,
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 24 hours
},
jwt: {
secret: process.env.JWT_SECRET,
},
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
// may have to switch it up a bit for other providers
if (account.provider === "google") {
// extract these two tokens
const { accessToken, idToken } = account;
// make a POST request to the DRF backend
try {
const response = await axios.post(
// tip: use a seperate .ts file or json file to store such URL endpoints
// "http://127.0.0.1:8000/api/social/login/google/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"social",
"login",
account.provider,
),
{
access_token: accessToken, // note the differences in key and value variable names
id_token: idToken,
},
);
// extract the returned token from the DRF backend and add it to the `user` object
const { access_token, refresh_token } = response.data;
user.accessToken = access_token;
user.refreshToken = refresh_token;
return true; // return true if everything went well
} catch (error) {
return false;
}
}
return false;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
const { accessToken, refreshToken } = user;
// reform the `token` object from the access token we appended to the `user` object
token = {
...token,
accessToken,
refreshToken,
};
// remove the tokens from the user objects just so that we don't leak it somehow
delete user.accessToken;
delete user.refreshToken;
return token;
}
// token has been invalidated, try refreshing it
if (JwtUtils.isJwtExpired(token.accessToken as string)) {
const [
newAccessToken,
newRefreshToken,
] = await NextAuthUtils.refreshToken(token.refreshToken);
if (newAccessToken && newRefreshToken) {
token = {
...token,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
// unable to refresh tokens from DRF backend, invalidate the token
return {
...token,
exp: 0,
};
}
// token valid
return token;
},
async session(session, userOrToken) {
session.accessToken = userOrToken.accessToken;
return session;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, settings);
接下来,the example in the Next-Auth documentation展示了useSession()
钩子的使用。但我不喜欢它,因为:
- 一旦访问令牌刷新,它不会更新会话状态,除非 window 本身被刷新(它是一个 open issue)
- 感觉在每个想要使用会话的组件上都有很多代码重复,还有检查会话对象是否存在、会话是否正在加载等的守卫。所以我想使用 HOC。
因此,我想出了以下解决方案:
constants/Hooks.tsx
import { Session } from "next-auth";
import { useEffect, useMemo, useState } from "react";
export function useAuth(refreshInterval?: number): [Session, boolean] {
/*
custom hook that keeps the session up-to-date by refreshing it
@param {number} refreshInterval: The refresh/polling interval in seconds. default is 10.
@return {tuple} A tuple of the Session and boolean
*/
const [session, setSession] = useState<Session>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchSession() {
let sessionData: Session = null;
setLoading(true);
const response = await fetch("/api/auth/session");
if (response.ok) {
const data: Session = await response.json();
if (Object.keys(data).length > 0) {
sessionData = data;
}
}
setSession(sessionData);
setLoading(false);
}
refreshInterval = refreshInterval || 10;
fetchSession();
const interval = setInterval(() => fetchSession(), refreshInterval * 1000);
return () => clearInterval(interval);
}, []);
return [session, loading];
}
constants/HOCs.tsx
import { Session } from "next-auth";
import { signIn } from "next-auth/client";
import React from "react";
import { useAuth } from "./Hooks";
type TSessionProps = {
session: Session;
};
export function withAuth<P extends object>(Component: React.ComponentType<P>) {
return React.memo(function (props: Exclude<P, TSessionProps>) {
const [session, loading] = useAuth(); // custom hook call
if (loading) {
return <h2>Loading...</h2>;
}
if (!loading && !session) {
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
<pre>{!session && "User is not logged in"}</pre>
</>
);
}
return <Component {...props} session={session} />;
});
}
然后,在我有定期数据获取要求的组件中(我知道这可以通过更好的方式实现,这只是一个人为的例子,我试图模拟用户不活动但应用程序仍然可以工作如果需要在后台),我正在使用 HOC:
pages/posts.tsx
import React, { useEffect, useState } from "react";
import Post from "../components/Post";
import { withAuth } from "../constants/HOCs";
import { TPost } from "../constants/Types";
import Link from "next/link";
function Posts(props) {
const { session } = props;
// const [session, loading] = useAuth();
const [posts, setPosts] = useState<TPost[]>([]);
const [fetchingPosts, setFetchingPosts] = useState<boolean>(false);
useEffect(() => {
if (!session) {
return;
}
async function getPosts() {
setFetchingPosts(true);
const response = await fetch("http://127.0.0.1:8000/api/posts", {
method: "get",
headers: new Headers({
Authorization: `Bearer ${session?.accessToken}`,
}),
});
if (response.ok) {
const posts: TPost[] = await response.json();
setPosts(posts);
}
setFetchingPosts(false);
}
// initiate the post fetching mechanism once
getPosts();
const intervalId = setInterval(() => getPosts(), 10 * 1000);
// useEffect cleanup
return () => clearInterval(intervalId);
}, [JSON.stringify(session)]);
// {
// loading && <h2>Loading...</h2>;
// }
// {
// !loading && !session && (
// <>
// Not signed in <br />
// <button onClick={() => signIn()}>Sign in</button>
// <pre>{!session && "User is not logged in"}</pre>
// </>
// );
// }
return (
<div>
<h2>Fetched at {JSON.stringify(new Date())}</h2>
<Link href="/">Back to homepage</Link>
{posts.map((post) => (
<Post key={post.title} post={post} />
))}
</div>
);
}
export default withAuth(Posts);
问题是由于 withAuth
HOC 以及可能由于 useAuth
钩子每 10 秒重新呈现整个页面。但是,我没有运气尝试调试它。也许我在我的 React 概念中遗漏了一些关键的东西。我感谢所有 suggestions/help 可能。提前致谢。
PS。我知道使用 SWR 库的解决方案,但我想尽可能避免使用该库。
在花费大量时间尝试解决此问题后,我最终使用了 useSwr()
挂钩。也写完了this article给有兴趣的人。
对于上下文,我正在将 Django Rest Framework 后端与 Next.js + Next-Auth 集成。除了一部分,我已经完成了大部分集成。要求是有一个刷新令牌系统,该系统将在访问令牌快过期时尝试刷新访问令牌。这是我的逻辑:
/api/auth/[...nextauth].ts
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import { NextAuthOptions } from "next-auth";
import Providers from "next-auth/providers";
import axios from "axios";
import { AuthenticatedUser } from "../../../types";
import { JwtUtils, UrlUtils } from "../../../constants/Utils";
namespace NextAuthUtils {
export const refreshToken = async function (refreshToken) {
try {
const response = await axios.post(
// "http://localhost:8000/api/auth/token/refresh/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"auth",
"token",
"refresh",
),
{
refresh: refreshToken,
},
);
const { access, refresh } = response.data;
// still within this block, return true
return [access, refresh];
} catch {
return [null, null];
}
};
}
const settings: NextAuthOptions = {
secret: process.env.SESSION_SECRET,
session: {
jwt: true,
maxAge: 24 * 60 * 60, // 24 hours
},
jwt: {
secret: process.env.JWT_SECRET,
},
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
async signIn(user: AuthenticatedUser, account, profile) {
// may have to switch it up a bit for other providers
if (account.provider === "google") {
// extract these two tokens
const { accessToken, idToken } = account;
// make a POST request to the DRF backend
try {
const response = await axios.post(
// tip: use a seperate .ts file or json file to store such URL endpoints
// "http://127.0.0.1:8000/api/social/login/google/",
UrlUtils.makeUrl(
process.env.BACKEND_API_BASE,
"social",
"login",
account.provider,
),
{
access_token: accessToken, // note the differences in key and value variable names
id_token: idToken,
},
);
// extract the returned token from the DRF backend and add it to the `user` object
const { access_token, refresh_token } = response.data;
user.accessToken = access_token;
user.refreshToken = refresh_token;
return true; // return true if everything went well
} catch (error) {
return false;
}
}
return false;
},
async jwt(token, user: AuthenticatedUser, account, profile, isNewUser) {
if (user) {
const { accessToken, refreshToken } = user;
// reform the `token` object from the access token we appended to the `user` object
token = {
...token,
accessToken,
refreshToken,
};
// remove the tokens from the user objects just so that we don't leak it somehow
delete user.accessToken;
delete user.refreshToken;
return token;
}
// token has been invalidated, try refreshing it
if (JwtUtils.isJwtExpired(token.accessToken as string)) {
const [
newAccessToken,
newRefreshToken,
] = await NextAuthUtils.refreshToken(token.refreshToken);
if (newAccessToken && newRefreshToken) {
token = {
...token,
accessToken: newAccessToken,
refreshToken: newRefreshToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
// unable to refresh tokens from DRF backend, invalidate the token
return {
...token,
exp: 0,
};
}
// token valid
return token;
},
async session(session, userOrToken) {
session.accessToken = userOrToken.accessToken;
return session;
},
},
};
export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, settings);
接下来,the example in the Next-Auth documentation展示了useSession()
钩子的使用。但我不喜欢它,因为:
- 一旦访问令牌刷新,它不会更新会话状态,除非 window 本身被刷新(它是一个 open issue)
- 感觉在每个想要使用会话的组件上都有很多代码重复,还有检查会话对象是否存在、会话是否正在加载等的守卫。所以我想使用 HOC。
因此,我想出了以下解决方案:
constants/Hooks.tsx
import { Session } from "next-auth";
import { useEffect, useMemo, useState } from "react";
export function useAuth(refreshInterval?: number): [Session, boolean] {
/*
custom hook that keeps the session up-to-date by refreshing it
@param {number} refreshInterval: The refresh/polling interval in seconds. default is 10.
@return {tuple} A tuple of the Session and boolean
*/
const [session, setSession] = useState<Session>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
async function fetchSession() {
let sessionData: Session = null;
setLoading(true);
const response = await fetch("/api/auth/session");
if (response.ok) {
const data: Session = await response.json();
if (Object.keys(data).length > 0) {
sessionData = data;
}
}
setSession(sessionData);
setLoading(false);
}
refreshInterval = refreshInterval || 10;
fetchSession();
const interval = setInterval(() => fetchSession(), refreshInterval * 1000);
return () => clearInterval(interval);
}, []);
return [session, loading];
}
constants/HOCs.tsx
import { Session } from "next-auth";
import { signIn } from "next-auth/client";
import React from "react";
import { useAuth } from "./Hooks";
type TSessionProps = {
session: Session;
};
export function withAuth<P extends object>(Component: React.ComponentType<P>) {
return React.memo(function (props: Exclude<P, TSessionProps>) {
const [session, loading] = useAuth(); // custom hook call
if (loading) {
return <h2>Loading...</h2>;
}
if (!loading && !session) {
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
<pre>{!session && "User is not logged in"}</pre>
</>
);
}
return <Component {...props} session={session} />;
});
}
然后,在我有定期数据获取要求的组件中(我知道这可以通过更好的方式实现,这只是一个人为的例子,我试图模拟用户不活动但应用程序仍然可以工作如果需要在后台),我正在使用 HOC:
pages/posts.tsx
import React, { useEffect, useState } from "react";
import Post from "../components/Post";
import { withAuth } from "../constants/HOCs";
import { TPost } from "../constants/Types";
import Link from "next/link";
function Posts(props) {
const { session } = props;
// const [session, loading] = useAuth();
const [posts, setPosts] = useState<TPost[]>([]);
const [fetchingPosts, setFetchingPosts] = useState<boolean>(false);
useEffect(() => {
if (!session) {
return;
}
async function getPosts() {
setFetchingPosts(true);
const response = await fetch("http://127.0.0.1:8000/api/posts", {
method: "get",
headers: new Headers({
Authorization: `Bearer ${session?.accessToken}`,
}),
});
if (response.ok) {
const posts: TPost[] = await response.json();
setPosts(posts);
}
setFetchingPosts(false);
}
// initiate the post fetching mechanism once
getPosts();
const intervalId = setInterval(() => getPosts(), 10 * 1000);
// useEffect cleanup
return () => clearInterval(intervalId);
}, [JSON.stringify(session)]);
// {
// loading && <h2>Loading...</h2>;
// }
// {
// !loading && !session && (
// <>
// Not signed in <br />
// <button onClick={() => signIn()}>Sign in</button>
// <pre>{!session && "User is not logged in"}</pre>
// </>
// );
// }
return (
<div>
<h2>Fetched at {JSON.stringify(new Date())}</h2>
<Link href="/">Back to homepage</Link>
{posts.map((post) => (
<Post key={post.title} post={post} />
))}
</div>
);
}
export default withAuth(Posts);
问题是由于 withAuth
HOC 以及可能由于 useAuth
钩子每 10 秒重新呈现整个页面。但是,我没有运气尝试调试它。也许我在我的 React 概念中遗漏了一些关键的东西。我感谢所有 suggestions/help 可能。提前致谢。
PS。我知道使用 SWR 库的解决方案,但我想尽可能避免使用该库。
在花费大量时间尝试解决此问题后,我最终使用了 useSwr()
挂钩。也写完了this article给有兴趣的人。