fix: unattended component reload due to session changes (#407)

* fix: unattended component reload due to session changes

When the session is updated, status goes back to loading then
authenticated or unauthenticated. session.accessTokenExpires may also be
updated.

This triggered various component refresh at unattented times, and brake
the user experience.

By splitting to only what's needed with memoization, it will prevent
unattented refresh.

* review

* review: change syntax
This commit is contained in:
2024-09-05 10:46:47 -06:00
committed by GitHub
parent c5f7fcc06b
commit db714f6390
9 changed files with 124 additions and 37 deletions

View File

@@ -3,38 +3,35 @@ import { useContext, useEffect, useState } from "react";
import { DomainContext, featureEnabled } from "../domainContext";
import { OpenApi, DefaultService } from "../api";
import { CustomSession } from "./types";
import useSessionStatus from "./useSessionStatus";
import useSessionAccessToken from "./useSessionAccessToken";
export default function useApi(): DefaultService | null {
const api_url = useContext(DomainContext).api_url;
const [api, setApi] = useState<OpenApi | null>(null);
const { data: session, status } = useSession();
const customSession = session as CustomSession;
const accessToken = customSession?.accessToken;
const { isLoading, isAuthenticated } = useSessionStatus();
const { accessToken, error } = useSessionAccessToken();
if (!api_url) throw new Error("no API URL");
useEffect(() => {
if (customSession?.error === "RefreshAccessTokenError") {
if (error === "RefreshAccessTokenError") {
signOut();
}
}, [session]);
}, [error]);
useEffect(() => {
if (status === "loading") {
return;
}
if (status === "authenticated" && !accessToken) {
if (isLoading || (isAuthenticated && !accessToken)) {
return;
}
const openApi = new OpenApi({
BASE: api_url,
TOKEN: accessToken,
TOKEN: accessToken || undefined,
});
setApi(openApi);
}, [accessToken, status]);
}, [isLoading, isAuthenticated, accessToken]);
return api?.default ?? null;
}

View File

@@ -0,0 +1,42 @@
"use client";
import { useState, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { CustomSession } from "./types";
export default function useSessionAccessToken() {
const { data: session } = useNextAuthSession();
const customSession = session as CustomSession;
const naAccessToken = customSession?.accessToken;
const naAccessTokenExpires = customSession?.accessTokenExpires;
const naError = customSession?.error;
const [accessToken, setAccessToken] = useState<string | null>(null);
const [accessTokenExpires, setAccessTokenExpires] = useState<number | null>(
null,
);
const [error, setError] = useState<string | undefined>();
useEffect(() => {
if (naAccessToken !== accessToken) {
setAccessToken(naAccessToken);
}
}, [naAccessToken]);
useEffect(() => {
if (naAccessTokenExpires !== accessTokenExpires) {
setAccessTokenExpires(naAccessTokenExpires);
}
}, [naAccessTokenExpires]);
useEffect(() => {
if (naError !== error) {
setError(naError);
}
}, [naError]);
return {
accessToken,
accessTokenExpires,
error,
};
}

View File

@@ -0,0 +1,22 @@
"use client";
import { useState, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { Session } from "next-auth";
export default function useSessionStatus() {
const { status: naStatus } = useNextAuthSession();
const [status, setStatus] = useState("loading");
useEffect(() => {
if (naStatus !== "loading" && naStatus !== status) {
setStatus(naStatus);
}
}, [naStatus]);
return {
status,
isLoading: status === "loading",
isAuthenticated: status === "authenticated",
};
}

View File

@@ -0,0 +1,33 @@
"use client";
import { useState, useEffect } from "react";
import { useSession as useNextAuthSession } from "next-auth/react";
import { Session } from "next-auth";
// user type with id, name, email
export interface User {
id?: string | null;
name?: string | null;
email?: string | null;
}
export default function useSessionUser() {
const { data: session } = useNextAuthSession();
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
if (!session?.user) {
setUser(null);
return;
}
if (JSON.stringify(session.user) !== JSON.stringify(user)) {
setUser(session.user);
}
}, [session]);
return {
id: user?.id,
name: user?.name,
email: user?.email,
};
}