fix: remove almost all window.reloads()

This commit is contained in:
zk
2026-05-11 17:54:14 +00:00
parent 66914fe857
commit f910bb8409
8 changed files with 110 additions and 27 deletions
@@ -14,9 +14,10 @@ import SelectLinkButton from "#components/util/SelectLinkButton";
import { TachiConfig } from "#lib/config";
import { type SetState } from "#types/react";
import { APIFetchV1 } from "#util/api";
import { allPermissions, DelayedPageReload } from "#util/misc";
import { allPermissions } from "#util/misc";
import React, { useEffect, useReducer, useState } from "react";
import { Alert, Button, Col, Form, Modal, Row } from "react-bootstrap";
import { useQueryClient } from "react-query";
import { Link, Route, Switch } from "react-router-dom";
import {
type APIPermissions,
@@ -167,7 +168,7 @@ function CreateNewOAuthClient({ setClients }: { setClients: SetState<TachiAPICli
onSubmit={async (e) => {
e.preventDefault();
const res = await APIFetchV1(
const res = await APIFetchV1<TachiAPIClientDocument>(
"/clients/create",
{
method: "POST",
@@ -188,7 +189,8 @@ function CreateNewOAuthClient({ setClients }: { setClients: SetState<TachiAPICli
);
if (res.success) {
DelayedPageReload();
setClients((prev) => [...prev, res.body]);
setShow(false);
}
}}
>
@@ -696,9 +698,9 @@ function KAIIntegrationStatus({
kaiType: "eag" | "flo" | "min";
userID: integer;
}) {
const { data, error } = useApiQuery<{ authStatus: boolean }>(
`/users/${userID}/integrations/kai/${kaiType}`,
);
const queryClient = useQueryClient();
const kaiQueryKey = `/users/${userID}/integrations/kai/${kaiType}`;
const { data, error } = useApiQuery<{ authStatus: boolean }>(kaiQueryKey);
if (error) {
return <ApiError error={error} />;
@@ -730,7 +732,7 @@ function KAIIntegrationStatus({
);
if (res.success) {
window.location.reload();
await queryClient.invalidateQueries([kaiQueryKey]);
}
}}
variant="danger"
@@ -7,10 +7,11 @@ import Loading from "#components/util/Loading";
import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import { APIFetchV1 } from "#util/api";
import { CopyToClipboard, DelayedPageReload } from "#util/misc";
import { CopyToClipboard } from "#util/misc";
import { FormatTime } from "#util/time";
import React from "react";
import { Button, Col, Row } from "react-bootstrap";
import { useQueryClient } from "react-query";
import { Link } from "react-router-dom";
import {
type integer,
@@ -26,6 +27,8 @@ export default function UserInvitesPage({ reqUser }: { reqUser: UserDocument })
`${reqUser.username}'s Invites`,
);
const queryClient = useQueryClient();
const { data, error } = useApiQuery<{ invites: integer; limit: integer }>(
`/users/${reqUser.id}/invites/limit`,
);
@@ -72,7 +75,14 @@ export default function UserInvitesPage({ reqUser }: { reqUser: UserDocument })
);
if (r.success) {
DelayedPageReload();
await Promise.all([
queryClient.invalidateQueries([
`/users/${reqUser.id}/invites`,
]),
queryClient.invalidateQueries([
`/users/${reqUser.id}/invites/limit`,
]),
]);
}
}}
>
@@ -6,10 +6,12 @@ import Icon from "#components/util/Icon";
import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import SelectButton from "#components/util/SelectButton";
import { BackgroundContext } from "#context/BackgroundContext";
import { UserContext } from "#context/UserContext";
import { UserSettingsContext } from "#context/UserSettingsContext";
import { type SetState } from "#types/react";
import { APIFetchV1, ToAPIURL } from "#util/api";
import { DelayedPageReload, FetchJSONBody, UppercaseFirst } from "#util/misc";
import { FetchJSONBody, UppercaseFirst } from "#util/misc";
import { getStoredTheme, mediaQueryPrefers, setTheme, type Themes } from "#util/themeUtils";
import { useFormik } from "formik";
import React, { useContext, useEffect, useRef, useState } from "react";
@@ -19,6 +21,7 @@ import Form from "react-bootstrap/Form";
import InputGroup from "react-bootstrap/InputGroup";
import Stack from "react-bootstrap/Stack";
import toast from "react-hot-toast";
import { useQueryClient } from "react-query";
import { type integer, type UserDocument, type UserSettingsDocument } from "tachi-common";
interface Props {
@@ -537,6 +540,13 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
const pfpInput = useRef<HTMLInputElement>(null);
const [banner, setBanner] = useState<File | undefined>();
const bannerInput = useRef<HTMLInputElement>(null);
const [mediaNonce, setMediaNonce] = useState(0);
const queryClient = useQueryClient();
const { setBackground } = useContext(BackgroundContext);
const { user: ctxUser, setUser } = useContext(UserContext);
const mediaQs = mediaNonce > 0 ? `?v=${mediaNonce}` : "";
const handleReset = (ref: React.MutableRefObject<HTMLInputElement | null>) => {
if (ref.current) {
@@ -544,6 +554,23 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
}
};
const afterMediaMutation = async (uploadType: "banner" | "pfp") => {
setMediaNonce((n) => n + 1);
if (uploadType === "banner") {
setBackground(ToAPIURL(`/users/${reqUser.id}/banner?v=${Date.now()}`));
}
await Promise.all([
queryClient.invalidateQueries([`/users/${reqUser.username}`]),
queryClient.invalidateQueries([`/users/${reqUser.id}`]),
]);
if (ctxUser?.id === reqUser.id) {
const r = await APIFetchV1<UserDocument>("/users/me", {}, true, true);
if (r.success) {
setUser(r.body);
}
}
};
return (
<Stack gap={4}>
<Alert variant="danger">
@@ -563,12 +590,17 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
/>
<div className="d-flex justify-content-center my-4">
<ProfilePicture
src={pfp ? URL.createObjectURL(pfp) : ToAPIURL(`/users/${reqUser.id}/pfp`)}
src={
pfp
? URL.createObjectURL(pfp)
: ToAPIURL(`/users/${reqUser.id}/pfp`) + mediaQs
}
user={reqUser}
/>
</div>
<FileUploadController
file={pfp}
onMutationSuccess={afterMediaMutation}
reqUser={reqUser}
reset={() => handleReset(pfpInput)}
setFile={setPfp}
@@ -592,11 +624,12 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
src={
banner
? URL.createObjectURL(banner)
: ToAPIURL(`/users/${reqUser.id}/banner`)
: ToAPIURL(`/users/${reqUser.id}/banner`) + mediaQs
}
/>
<FileUploadController
file={banner}
onMutationSuccess={afterMediaMutation}
reqUser={reqUser}
reset={() => handleReset(bannerInput)}
setFile={setBanner}
@@ -608,6 +641,7 @@ function ImageForm({ reqUser }: { reqUser: UserDocument }) {
}
function SocialMediaForm({ reqUser }: { reqUser: UserDocument }) {
const queryClient = useQueryClient();
const placeholders = {
discord: "Discord Username",
twitter: "Twitter Handle",
@@ -648,7 +682,10 @@ function SocialMediaForm({ reqUser }: { reqUser: UserDocument }) {
);
if (rj.success) {
DelayedPageReload();
await Promise.all([
queryClient.invalidateQueries([`/users/${reqUser.username}`]),
queryClient.invalidateQueries([`/users/${reqUser.id}`]),
]);
}
},
});
@@ -710,8 +747,10 @@ function FileUploadController({
reqUser,
setFile,
reset,
onMutationSuccess,
}: {
file?: File;
onMutationSuccess: (uploadType: "banner" | "pfp") => Promise<void> | void;
reqUser: UserDocument;
reset: () => void;
setFile: SetState<File | undefined>;
@@ -750,7 +789,7 @@ function FileUploadController({
);
if (res.success) {
DelayedPageReload();
await onMutationSuccess(type);
}
}
}}
@@ -782,7 +821,9 @@ function FileUploadController({
);
if (res.success) {
window.location.reload();
setFile(undefined);
reset();
await onMutationSuccess(type);
}
}}
variant="success"
@@ -1,11 +1,12 @@
import Icon from "#components/util/Icon";
import { useInvalidateUseApiQueryCache } from "#components/util/query/useApiQuery";
import { APIFetchV1 } from "#util/api";
import { DelayedPageReload } from "#util/misc";
import React, { useMemo, useReducer } from "react";
import { type ScoreDocument } from "tachi-common";
export default function DeleteScoreBtn({ score }: { score: ScoreDocument }) {
const [warn, upgWarn] = useReducer((r) => r + 1, 0);
const invalidateApiQueries = useInvalidateUseApiQueryCache();
const message = useMemo(() => {
if (warn === 0) {
return <Icon noPad type="trash" />;
@@ -36,7 +37,7 @@ export default function DeleteScoreBtn({ score }: { score: ScoreDocument }) {
},
true,
true,
).then(() => DelayedPageReload());
).then(() => invalidateApiQueries());
}
}}
>
@@ -1,11 +1,12 @@
import { useInvalidateUseApiQueryCache } from "#components/util/query/useApiQuery";
import { APIFetchV1 } from "#util/api";
import { DelayedPageReload } from "#util/misc";
import React, { useMemo, useReducer } from "react";
import { Button } from "react-bootstrap";
import { type ImportDocument } from "tachi-common";
export default function ManageImport({ importDoc }: { importDoc: ImportDocument }) {
const [warn, upgWarn] = useReducer((r) => r + 1, 0);
const invalidateApiQueries = useInvalidateUseApiQueryCache();
const message = useMemo(() => {
if (warn === 0) {
return "Undo Import (Requires Further Confirmation)";
@@ -39,7 +40,7 @@ export default function ManageImport({ importDoc }: { importDoc: ImportDocument
},
true,
true,
).then(() => DelayedPageReload());
).then(() => invalidateApiQueries());
}
}}
variant="danger"
@@ -11,6 +11,7 @@ import { APIFetchV1 } from "#util/api";
import { FormatDate } from "#util/time";
import React, { useContext, useState } from "react";
import { Button, Form, InputGroup, Modal } from "react-bootstrap";
import { useQueryClient } from "react-query";
import { UserAuthLevels, type UserDocument } from "tachi-common";
import ProfileBadges from "./ProfileBadges";
@@ -42,12 +43,20 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) {
}
const { user: loggedInUser } = useContext(UserContext);
const queryClient = useQueryClient();
async function invalidateProfileQueries() {
await Promise.all([
queryClient.invalidateQueries([`/users/${reqUser.username}`]),
queryClient.invalidateQueries([`/users/${reqUser.id}`]),
]);
}
return (
<>
<div className="col-12 col-lg-3">
<div className="d-flex justify-content-center mb-3">
<ProfilePicture user={reqUser} />
<ProfilePicture key={reqUser.customPfpLocation ?? "default"} user={reqUser} />
</div>
<div className="d-flex align-items-center" style={{ flexDirection: "column" }}>
<ProfileBadges user={reqUser} />
@@ -69,7 +78,11 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) {
{ method: "DELETE" },
true,
true,
).then(() => window.location.reload())
).then(async (res) => {
if (res.success) {
await invalidateProfileQueries();
}
})
}
variant="danger"
>
@@ -83,7 +96,11 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) {
{ method: "POST" },
true,
true,
).then(() => window.location.reload())
).then(async (res) => {
if (res.success) {
await invalidateProfileQueries();
}
})
}
variant="primary"
>
@@ -1,11 +1,26 @@
import { APIFetchV1, type UnsuccessfulAPIFetchResponse } from "#util/api";
import { type QueryKey, useQuery } from "react-query";
import { type QueryClient, type QueryKey, useQuery, useQueryClient } from "react-query";
import { type SuccessfulAPIResponse } from "tachi-common";
export function UnfuckURLsV3GameToV2Format(url: string) {
return url;
}
/** Invalidate data fetched via useApiQuery (keys are URL path strings) without a full page reload. */
export function invalidateUseApiQueryCache(queryClient: QueryClient) {
return queryClient.invalidateQueries({
predicate: (q) =>
Array.isArray(q.queryKey) &&
q.queryKey.length > 0 &&
q.queryKey.every((part) => typeof part === "string" && part.startsWith("/")),
});
}
export function useInvalidateUseApiQueryCache() {
const queryClient = useQueryClient();
return () => invalidateUseApiQueryCache(queryClient);
}
export default function useApiQuery<T>(
url: string | string[],
options?: RequestInit,
-4
View File
@@ -146,10 +146,6 @@ export function FormatMillions(v: number) {
return v.toLocaleString();
}
export function DelayedPageReload(delay = 300) {
setTimeout(() => window.location.reload(), delay);
}
export function ShortDelayify(fn: () => void, delay = 300) {
setTimeout(fn, delay);
}