Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ export default function CommunityDownloadsSubscriptions() {
const [totalResults, setTotalResults] = useState(0);
const [page, setPage] = useState(1);

function fetchResults(mypage: number) {
useEffect(() => {
let isMounted = true;

commonApiFetch<{ count: number; data: SubscriptionDownload[] }>({
endpoint: `subscriptions/uploads?contract=${MEMES_CONTRACT}&page_size=${PAGE_SIZE}&page=${mypage}`,
endpoint: `subscriptions/uploads?contract=${MEMES_CONTRACT}&page_size=${PAGE_SIZE}&page=${page}`,
}).then((response) => {
if (!isMounted) return;

setTotalResults(response.count);
setDownloads(response.data || []);
});
}

useEffect(() => {
fetchResults(page);
return () => {
isMounted = false;
};
}, [page]);

return (
Expand Down
16 changes: 9 additions & 7 deletions components/eula/EULAConsentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, {
useState,
useEffect,
useMemo,
useCallback,
ReactNode,
} from "react";
import { commonApiFetch, commonApiPost } from "@/services/api/common-api";
Expand Down Expand Up @@ -42,11 +43,12 @@ export const EULAConsentProvider: React.FC<EULAConsentProviderProps> = ({
const [showEULAConsent, setShowEULAConsent] = useState(false);

const capacitor = useCapacitor();
const { isIos, platform } = capacitor;

const getEULAConsent = async () => {
const getEULAConsent = useCallback(async () => {
try {
const eulaConsent = Cookies.get(CONSENT_EULA_COOKIE) === "true";
if (!eulaConsent && capacitor.isIos) {
if (!eulaConsent && isIos) {
const deviceId = await Device.getId();
const response = await commonApiFetch<{ accepted_at: number }>({
endpoint: `policies/eula-consent/${deviceId.identifier}`,
Expand All @@ -67,16 +69,16 @@ export const EULAConsentProvider: React.FC<EULAConsentProviderProps> = ({
} catch (error) {
console.error("Failed to fetch EULA consent status", error);
}
};
}, [isIos]);

const consent = async () => {
const consent = useCallback(async () => {
try {
const deviceId = await Device.getId();
await commonApiPost({
endpoint: `policies/eula-consent`,
body: {
device_id: deviceId.identifier,
platform: capacitor.platform,
platform,
},
});
Cookies.set(CONSENT_EULA_COOKIE, "true", { expires: 365 });
Expand All @@ -88,13 +90,13 @@ export const EULAConsentProvider: React.FC<EULAConsentProviderProps> = ({
message: "Something went wrong...",
});
}
};
}, [getEULAConsent, platform, setToast]);

const value = useMemo(() => ({ consent }), [consent]);

useEffect(() => {
getEULAConsent();
}, []);
}, [getEULAConsent]);

return (
<EULAConsentContext.Provider value={value}>
Expand Down
13 changes: 6 additions & 7 deletions components/gas-royalties/Gas.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Container, Row, Col, Table } from "react-bootstrap";
import styles from "./GasRoyalties.module.scss";
import { Gas } from "@/entities/IGas";
Expand Down Expand Up @@ -60,11 +60,9 @@ export default function GasComponent() {
toBlock,
} = useSharedState();

function getUrlWithParams() {
return getUrl("gas");
}
const getUrlWithParams = useCallback(() => getUrl("gas"), [getUrl]);

function fetchGas() {
const fetchGas = useCallback(() => {
setFetching(true);
fetchUrl(getUrlWithParams()).then((res: Gas[]) => {
res.forEach((r) => {
Expand All @@ -74,7 +72,7 @@ export default function GasComponent() {
setSumGas(res.map((g) => g.gas).reduce((a, b) => a + b, 0));
setFetching(false);
});
}
}, [getUrlWithParams]);

useEffect(() => {
if (collectionFocus) {
Expand All @@ -89,14 +87,15 @@ export default function GasComponent() {
selectedArtist,
isPrimary,
isCustomBlocks,
fetchGas,
]);

useEffect(() => {
if (collectionFocus) {
setGas([]);
fetchGas();
}
}, [collectionFocus]);
}, [collectionFocus, fetchGas]);

if (!collectionFocus) {
return <></>;
Expand Down
6 changes: 3 additions & 3 deletions components/gas-royalties/Royalties.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useState, useEffectEvent } from "react";
import { Container, Row, Col, Table } from "react-bootstrap";
import styles from "./GasRoyalties.module.scss";
import { Royalty } from "@/entities/IRoyalty";
Expand Down Expand Up @@ -71,7 +71,7 @@ export default function RoyaltiesComponent() {
return getUrl("royalties");
}

function fetchRoyalties() {
const fetchRoyalties = useEffectEvent(() => {
setFetching(true);
fetchUrl(getUrlWithParams()).then((res: Royalty[]) => {
res.forEach((r) => {
Expand All @@ -88,7 +88,7 @@ export default function RoyaltiesComponent() {
);
setFetching(false);
});
}
});

useEffect(() => {
if (collectionFocus) {
Expand Down
48 changes: 32 additions & 16 deletions components/groups/page/Groups.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { useContext, useEffect, useState, type JSX } from "react";
import {
useCallback,
useContext,
useEffect,
useState,
type JSX,
} from "react";
import GroupCreate from "./create/GroupCreate";
import { AuthContext } from "@/components/auth/Auth";
import GroupsPageListWrapper from "./GroupsPageListWrapper";
Expand Down Expand Up @@ -28,28 +34,38 @@ export default function Groups() {

const [viewMode, setViewMode] = useState(GroupsViewMode.VIEW);

const onViewModeChange = async (mode: GroupsViewMode): Promise<void> => {
if (mode === GroupsViewMode.CREATE) {
const { success } = await requestAuth();
if (!success) return;
} else if (pathname) {
router.replace(pathname);
}
const onViewModeChange = useCallback(
async (mode: GroupsViewMode): Promise<void> => {
if (mode === GroupsViewMode.CREATE) {
const { success } = await requestAuth();
if (!success) return;
} else if (pathname) {
router.replace(pathname);
}

setViewMode(mode);
},
[pathname, requestAuth, router],
);
Comment on lines +37 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add mounted guard to prevent state updates after unmount.

The PR summary indicates that "mounted guards and cancellation mechanisms are introduced for async operations," but this async callback lacks a mounted check before calling setViewMode(mode) on line 46. If the component unmounts while authentication is pending, this will trigger React warnings and potentially cause memory leaks.

Apply this diff to add a mounted guard:

+  const mountedRef = useRef(true);
+
+  useEffect(() => {
+    return () => {
+      mountedRef.current = false;
+    };
+  }, []);
+
  const onViewModeChange = useCallback(
    async (mode: GroupsViewMode): Promise<void> => {
      if (mode === GroupsViewMode.CREATE) {
        const { success } = await requestAuth();
        if (!success) return;
      } else if (pathname) {
        router.replace(pathname);
      }

+      if (!mountedRef.current) return;
      setViewMode(mode);
    },
    [pathname, requestAuth, router],
  );

Don't forget to add useRef to the imports on line 3.

🤖 Prompt for AI Agents
In components/groups/page/Groups.tsx around lines 37 to 49, the async
onViewModeChange callback can call setViewMode after the component has
unmounted; add a mounted guard using useRef (add useRef to the imports on line
3), set mountedRef.current = true in a useEffect and flip to false on cleanup,
then check mountedRef.current before calling setViewMode(mode) (and also after
awaiting requestAuth) to avoid state updates after unmount; keep the rest of the
logic intact.


setViewMode(mode);
};
const hasConnectedProfile = Boolean(connectedProfile?.handle);
const isProxyActive = Boolean(activeProfileProxy);
const shouldAutoEnableCreate = Boolean(
edit && hasConnectedProfile && !isProxyActive,
);
const shouldForceViewMode = !hasConnectedProfile || isProxyActive;

useEffect(() => {
if (edit && !!connectedProfile?.handle && !activeProfileProxy) {
onViewModeChange(GroupsViewMode.CREATE);
if (shouldAutoEnableCreate) {
void onViewModeChange(GroupsViewMode.CREATE);
}
}, [edit]);
}, [shouldAutoEnableCreate, onViewModeChange]);

useEffect(() => {
if (!connectedProfile?.handle || activeProfileProxy) {
onViewModeChange(GroupsViewMode.VIEW);
if (shouldForceViewMode) {
void onViewModeChange(GroupsViewMode.VIEW);
}
}, [connectedProfile, activeProfileProxy]);
}, [shouldForceViewMode, onViewModeChange]);

const components: Record<GroupsViewMode, JSX.Element> = {
[GroupsViewMode.VIEW]: (
Expand Down
10 changes: 8 additions & 2 deletions components/groups/page/list/card/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ApiGroupFull } from "@/generated/models/ApiGroupFull";
import { ApiRateMatter } from "@/generated/models/ApiRateMatter";
import { getRandomColorWithSeed } from "@/helpers/Helpers";
import { useRouter } from "next/navigation";
import { useContext, useEffect, useState, type JSX } from "react";
import { useContext, useEffect, useEffectEvent, useState, type JSX } from "react";
import GroupCardView from "./GroupCardView";
import GroupCardVoteAll from "./vote-all/GroupCardVoteAll";

Expand Down Expand Up @@ -54,6 +54,12 @@ export default function GroupCard({
setState(state);
};

const resetGroupState = useEffectEvent(() => {
if (!setActiveGroupIdVoteAll || !group) return;
setActiveGroupIdVoteAll(null);
setState(GroupCardState.IDLE);
});

const getIsActiveGroupVoteAll = () => {
return activeGroupIdVoteAll === group?.id;
};
Expand All @@ -70,7 +76,7 @@ export default function GroupCard({

useEffect(() => {
if (!connectedProfile?.handle) {
onGroupStateChange(GroupCardState.IDLE);
resetGroupState();
}
}, [connectedProfile?.handle]);

Expand Down
7 changes: 1 addition & 6 deletions components/groups/page/list/card/GroupCardActionWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { useEffect, useState } from "react";
import GroupCardActionFooter from "./utils/GroupCardActionFooter";
import { ApiRateMatter } from "@/generated/models/ApiRateMatter";

Expand Down Expand Up @@ -40,11 +39,7 @@ export default function GroupCardActionWrapper({
return `${(doneMembersCount / membersCount) * 100}%`;
};

const [progress, setProgress] = useState(getProgress());

useEffect(() => {
setProgress(getProgress());
}, [membersCount, doneMembersCount]);
const progress = getProgress();
return (
<div className="tw-flex tw-h-full tw-flex-col tw-gap-y-5 tw-px-4 tw-py-5 sm:tw-px-5 sm:tw-py-6">
<div className="tw-flex-1">
Expand Down
24 changes: 8 additions & 16 deletions components/groups/page/list/card/utils/GroupCardActionStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export default function GroupCardActionStats({
null
);

const [creditPerMember, setCreditPerMember] = useState<number>(0);

useEffect(() => {
if (!connectedProfile?.handle) {
setRater(null);
Expand Down Expand Up @@ -96,20 +94,14 @@ export default function GroupCardActionStats({
}
};

useEffect(() => {
const credit = getCreditLeft();
if (
typeof credit === "number" &&
typeof membersCount === "number" &&
credit > 0 &&
membersCount > 0
) {
const creditPerMember = credit / membersCount;
setCreditPerMember(creditPerMember);
} else {
setCreditPerMember(0);
}
}, [creditLeft, membersCount]);
const credit = getCreditLeft();
const creditPerMember =
typeof credit === "number" &&
typeof membersCount === "number" &&
credit > 0 &&
membersCount > 0
? credit / membersCount
: 0;

const count =
typeof membersCount === "number"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,12 @@ export default function GroupCardVoteAll({
return false;
};

const [disabled, setDisabled] = useState<boolean>(getIsDisabled());
const disabled = getIsDisabled();

useEffect(
() => setLoading(isFetching || doingRates),
[isFetching, doingRates]
);
useEffect(
() => setDisabled(getIsDisabled()),
[amountToAdd, membersCount, loading, category]
);

const bulkRateMutation = useMutation({
mutationFn: async (body: ApiBulkRateRequest) =>
Expand Down
11 changes: 2 additions & 9 deletions components/groups/select/item/GroupItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useState } from "react";
import {
ImageScale,
getScaledImageUri,
Expand All @@ -19,14 +19,7 @@ export default function GroupItem({
readonly activeGroupId: string | null;
readonly onActiveGroupId?: (groupId: string | null) => void;
}) {
const getIsActive = (): boolean =>
!!activeGroupId && activeGroupId === group.id;

const [isActive, setIsActive] = useState(getIsActive());

useEffect(() => {
setIsActive(getIsActive());
}, [activeGroupId]);
const isActive = !!activeGroupId && activeGroupId === group.id;

const deActivate = () => {
if (!isActive || !onActiveGroupId) return;
Expand Down
13 changes: 11 additions & 2 deletions components/header/header-search/HeaderSearchModalItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import useDeviceInfo from "@/hooks/useDeviceInfo";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect, useRef, type ComponentType } from "react";
import {
useEffect,
useEffectEvent,
useRef,
type ComponentType,
} from "react";
import { useHoverDirty } from "react-use";
import HeaderSearchModalItemMedia from "./HeaderSearchModalItemMedia";
import HeaderSearchModalPfp from "./HeaderSearchModalPfp";
Expand Down Expand Up @@ -140,9 +145,13 @@ export default function HeaderSearchModalItem({
}
};

const emitHover = useEffectEvent((state: boolean) => {
onHover(state);
});

useEffect(() => {
if (supportsHover) {
onHover(isHovering);
emitHover(isHovering);
}
}, [isHovering, supportsHover]);

Expand Down
Loading
Loading