Skip to content

Commit bdc564c

Browse files
committed
fix: Resolve React 19 compiler ESLint errors
- Replace useState+useEffect patterns with useSyncExternalStore for client-side detection (auth, ThemeToggle, Search) - Rewrite useLocalStorage hook to use useSyncExternalStore - Convert setState-in-effect patterns to useMemo where applicable - Move Placeholder component outside render to fix "cannot create components during render" error - Replace <a> with <Link> in company page - Simplify CustomTextareaAutosize to avoid mutating prop refs - Restructure settings page to avoid JSX in try/catch - Add eslint-disable comments for intentional patterns - Ignore cdk/ directory in ESLint config Reduces ESLint errors from 23 to 0 for deployment compatibility.
1 parent ee55633 commit bdc564c

15 files changed

Lines changed: 189 additions & 153 deletions

File tree

app/(app)/alpha/additional-details/_client.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useEffect, useState } from "react";
3+
import React, { useEffect, useMemo, useState } from "react";
44
import { redirect, useRouter, useSearchParams } from "next/navigation";
55
import { useSession } from "next-auth/react";
66
import {
@@ -228,23 +228,21 @@ function SlideTwo({ details }: { details: UserDetails }) {
228228
parsedDateOfBirth?.getDate(),
229229
);
230230

231-
const [listOfDaysInSelectedMonth, setListOfDaysInSelectedMonth] = useState([
232-
0,
233-
]);
234-
235-
useEffect(() => {
236-
// If year or month change, recalculate how many days are in the specified month
231+
// Compute days in month directly from year/month (no state needed)
232+
const listOfDaysInSelectedMonth = useMemo(() => {
237233
if (year && month !== undefined) {
238234
// Returns the last day of the month, by creating a date with day 0 of the following month.
239-
const nummberOfDaysInMonth = new Date(year, month + 1, 0).getDate();
240-
const daysArray = Array.from(
241-
{ length: nummberOfDaysInMonth },
235+
const numberOfDaysInMonth = new Date(year, month + 1, 0).getDate();
236+
return Array.from(
237+
{ length: numberOfDaysInMonth },
242238
(_, index) => index + 1,
243239
);
244-
setListOfDaysInSelectedMonth(daysArray);
245240
}
241+
return [0];
242+
}, [year, month]);
246243

247-
// Update the date object when year, month or date change
244+
// Update the date object when year, month or day change
245+
useEffect(() => {
248246
if (year && month !== undefined && day) {
249247
let selectedDate: Date;
250248

@@ -257,7 +255,7 @@ function SlideTwo({ details }: { details: UserDetails }) {
257255
}
258256
setValue("dateOfBirth", selectedDate.toISOString());
259257
}
260-
}, [year, month, day]);
258+
}, [year, month, day, setValue]);
261259

262260
const startYearAgeDropdown = 1950;
263261
const endYearAgeDropdown = 2010;

app/(app)/auth/page.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@ import Link from "next/link";
55
import Image from "next/image";
66
import { useTheme } from "next-themes";
77
import { THEME_MODES } from "@/components/Theme/ThemeToggle/ThemeToggle";
8-
import { useEffect, useState } from "react";
8+
import { useSyncExternalStore } from "react";
9+
10+
// Subscribe to nothing - this is just to detect client-side rendering
11+
const emptySubscribe = () => () => {};
12+
const getClientSnapshot = () => true;
13+
const getServerSnapshot = () => false;
914

1015
export const PostAuthPage = (content: {
1116
heading: string;
1217
subHeading: string;
1318
}) => {
14-
const [mounted, setMounted] = useState(false);
19+
const mounted = useSyncExternalStore(
20+
emptySubscribe,
21+
getClientSnapshot,
22+
getServerSnapshot,
23+
);
1524
const { resolvedTheme } = useTheme();
1625

17-
// useEffect only happens on client not server
18-
useEffect(() => {
19-
setMounted(true);
20-
}, []);
21-
2226
// if on server dont render. needed to prevent a hydration mismatch error
2327
if (!mounted) return null;
2428

app/(app)/company/[slug]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { notFound } from "next/navigation";
2+
import Link from "next/link";
23
import { companies } from "./config";
34

45
export const metadata = {
@@ -64,12 +65,12 @@ export default async function Page(props: Props) {
6465
</div>
6566
</div>
6667
<div className="border-neutral-200 bg-neutral-100 p-4 dark:border-neutral-700 dark:bg-neutral-800">
67-
<a
68+
<Link
6869
href="/sponsorship"
6970
className="text-sm font-medium text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300"
7071
>
7172
← Back to all sponsors
72-
</a>
73+
</Link>
7374
</div>
7475
</div>
7576
</div>

app/(app)/notifications/_client.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ import {
1212
import PageHeading from "@/components/PageHeading/PageHeading";
1313
import { api } from "@/server/trpc/react";
1414

15+
// Moved outside to avoid "cannot create components during render" error
16+
const Placeholder = () => (
17+
<div className="my-4 w-full border border-neutral-100 bg-neutral-100 p-4 shadow dark:border-white dark:bg-black">
18+
<div className="animate-pulse">
19+
<div className="flex space-x-4">
20+
<div className="h-10 w-10 rounded-full bg-gray-300 dark:bg-neutral-800"></div>
21+
<div className="flex-1 space-y-2 py-1">
22+
<div className="grid grid-cols-8 gap-4">
23+
<div className="col-span-6 h-4 rounded bg-gray-300 dark:bg-neutral-800"></div>
24+
<div className="col-span-3 h-2 rounded bg-gray-300 dark:bg-neutral-800"></div>
25+
</div>
26+
</div>
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
1532
const Notifications = () => {
1633
const {
1734
status,
@@ -50,26 +67,10 @@ const Notifications = () => {
5067
if (inView && hasNextPage) {
5168
fetchNextPage();
5269
}
53-
}, [inView]);
70+
}, [inView, hasNextPage, fetchNextPage]);
5471

5572
const noNotifications = !data?.pages[0].data.length;
5673

57-
const Placeholder = () => (
58-
<div className="my-4 w-full border border-neutral-100 bg-neutral-100 p-4 shadow dark:border-white dark:bg-black">
59-
<div className="animate-pulse">
60-
<div className="flex space-x-4">
61-
<div className="h-10 w-10 rounded-full bg-gray-300 dark:bg-neutral-800"></div>
62-
<div className="flex-1 space-y-2 py-1">
63-
<div className="grid grid-cols-8 gap-4">
64-
<div className="col-span-6 h-4 rounded bg-gray-300 dark:bg-neutral-800"></div>
65-
<div className="col-span-3 h-2 rounded bg-gray-300 dark:bg-neutral-800"></div>
66-
</div>
67-
</div>
68-
</div>
69-
</div>
70-
</div>
71-
);
72-
7374
return (
7475
<>
7576
<div className="relative mx-4 max-w-2xl sm:mx-auto">

app/(app)/settings/page.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,18 @@ export default async function Page() {
7070
return notFound();
7171
}
7272

73+
// Fetch newsletter status with error handling
74+
let newsletterStatus = existingUser.newsletter;
7375
try {
74-
const newsletter = await isUserSubscribedToNewsletter(session.user.email);
75-
const cleanedUser = {
76-
...existingUser,
77-
newsletter,
78-
};
79-
return <Content profile={cleanedUser} />;
76+
newsletterStatus = await isUserSubscribedToNewsletter(session.user.email);
8077
} catch (error) {
8178
Sentry.captureException(error);
82-
return <Content profile={existingUser} />;
79+
// Fall back to existing newsletter status
8380
}
81+
82+
const cleanedUser = {
83+
...existingUser,
84+
newsletter: newsletterStatus,
85+
};
86+
return <Content profile={cleanedUser} />;
8487
}

app/verify-email/_client.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Button } from "@headlessui/react";
44
import { AlertCircle, CheckCircle, Loader } from "lucide-react";
55
import { useRouter, useSearchParams } from "next/navigation";
6-
import React, { useEffect, useState } from "react";
6+
import React, { useEffect, useState, useRef } from "react";
77

88
function Content() {
99
const params = useSearchParams();
@@ -12,16 +12,14 @@ function Content() {
1212
"idle" | "pending" | "success" | "error"
1313
>("idle");
1414
const [message, setMessage] = useState("");
15-
const [token, setToken] = useState<string | null>(null);
15+
// Get token directly from params (no need for separate state)
16+
const token = params.get("token");
17+
const hasVerified = useRef(false);
1618

1719
useEffect(() => {
18-
const tokenParam = params.get("token");
19-
if (tokenParam && !token) {
20-
setToken(tokenParam);
21-
}
22-
}, [params, token]);
20+
// Prevent double verification in strict mode
21+
if (hasVerified.current) return;
2322

24-
useEffect(() => {
2523
const verifyEmail = async () => {
2624
if (!token) {
2725
setStatus("error");
@@ -30,6 +28,7 @@ function Content() {
3028
);
3129
return;
3230
}
31+
hasVerified.current = true;
3332
setStatus("pending");
3433

3534
try {
@@ -41,7 +40,7 @@ function Content() {
4140
setStatus("error");
4241
}
4342
setMessage(data.message);
44-
} catch (error) {
43+
} catch {
4544
setStatus("error");
4645
setMessage(
4746
"An error occurred during verification. Please try again later.",

components/ArticleMenu/ArticleMenu.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
PopoverPanel,
77
Transition,
88
} from "@headlessui/react";
9-
import React, { Fragment, useEffect, useState } from "react";
9+
import React, { Fragment, useEffect, useMemo, useState } from "react";
1010

1111
import { api } from "@/server/trpc/react";
1212

@@ -20,11 +20,6 @@ import { type Session } from "next-auth";
2020
import { signIn } from "next-auth/react";
2121
import { ReportModal } from "../ReportModal/ReportModal";
2222

23-
interface CopyToClipboardOption {
24-
label: string;
25-
href: string;
26-
}
27-
2823
interface Props {
2924
session: Session | null;
3025
postId: string;
@@ -41,25 +36,23 @@ const ArticleMenu = ({
4136
postUrl,
4237
}: Props) => {
4338
const [copied, setCopied] = useState<boolean>(false);
44-
const [copyToClipboard, setCopyToClipboard] = useState<CopyToClipboardOption>(
45-
{
46-
label: "",
47-
href: "",
48-
},
39+
40+
// Compute label from copied state (no side effect needed)
41+
const label = useMemo(
42+
() => (copied ? "Copied!" : "Copy to clipboard"),
43+
[copied],
4944
);
5045

51-
const { label, href } = copyToClipboard;
46+
// Get href on client side only
47+
const href = typeof window !== "undefined" ? window.location.href : "";
5248

5349
const { data, refetch } = api.post.sidebarData.useQuery({
5450
id: postId,
5551
});
5652

5753
useEffect(() => {
58-
setCopyToClipboard({
59-
label: copied ? "Copied!" : "Copy to clipboard",
60-
href: location.href,
61-
});
62-
const to = setTimeout(setCopied, 1000, false);
54+
if (!copied) return;
55+
const to = setTimeout(() => setCopied(false), 1000);
6356
return () => clearTimeout(to);
6457
}, [copied]);
6558

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,26 @@
1-
import type { Ref, ForwardRefRenderFunction } from "react";
2-
import React, { forwardRef } from "react";
1+
import type { ForwardRefRenderFunction } from "react";
2+
import React, { forwardRef, useImperativeHandle } from "react";
33
import type { TextareaAutosizeProps } from "react-textarea-autosize";
44
import TextareaAutosize from "react-textarea-autosize";
55

6-
interface TextareaAutosizeWrapperProps extends TextareaAutosizeProps {
7-
inputRef?: Ref<HTMLTextAreaElement>;
8-
}
9-
6+
// Simplified wrapper that only uses forwarded ref
107
const TextareaAutosizeWrapper: ForwardRefRenderFunction<
118
HTMLTextAreaElement,
12-
TextareaAutosizeWrapperProps
9+
TextareaAutosizeProps
1310
> = (props, ref) => {
14-
const { inputRef, ...rest } = props;
15-
16-
const combinedRef = (node: HTMLTextAreaElement | null) => {
17-
if (ref) {
18-
if (typeof ref === "function") {
19-
ref(node);
20-
} else {
21-
(ref as React.MutableRefObject<HTMLTextAreaElement | null>).current =
22-
node;
23-
}
24-
}
11+
const internalRef = React.useRef<HTMLTextAreaElement | null>(null);
2512

26-
if (inputRef) {
27-
if (typeof inputRef === "function") {
28-
inputRef(node);
29-
} else {
30-
(
31-
inputRef as React.MutableRefObject<HTMLTextAreaElement | null>
32-
).current = node;
33-
}
34-
}
35-
};
13+
// Use useImperativeHandle to safely expose the ref
14+
useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement, []);
3615

37-
return <TextareaAutosize ref={combinedRef} {...rest} />;
16+
return (
17+
<TextareaAutosize
18+
ref={(node) => {
19+
internalRef.current = node;
20+
}}
21+
{...props}
22+
/>
23+
);
3824
};
3925

4026
export default forwardRef(TextareaAutosizeWrapper);

components/Hero/Hero.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -140,25 +140,26 @@ const NightSky = () => {
140140
{ x: 7, y: 37 },
141141
];
142142

143-
const seededRandom = (function () {
144-
const seed = 12345; // You can change this seed to get a different, but consistent, pattern
143+
// Create a seeded random function (pure - based on index)
144+
const seededRandom = (index: number) => {
145+
const seed = 12345;
145146
let state = seed;
146-
return function () {
147+
for (let i = 0; i <= index; i++) {
147148
state = (state * 1664525 + 1013904223) % 4294967296;
148-
return state / 4294967296;
149-
};
150-
})();
149+
}
150+
return state / 4294967296;
151+
};
151152

152153
const generateStars = () => {
153154
return starPositions.map((pos, i) => (
154155
<circle
155156
key={i}
156157
cx={pos.x}
157158
cy={pos.y}
158-
r={seededRandom() * 0.15 + 0.05}
159+
r={seededRandom(i * 3) * 0.15 + 0.05}
159160
fill="white"
160-
opacity={seededRandom() * 0.5 + 0.3}
161-
className={seededRandom() > 0.7 ? "twinkle" : ""}
161+
opacity={seededRandom(i * 3 + 1) * 0.5 + 0.3}
162+
className={seededRandom(i * 3 + 2) > 0.7 ? "twinkle" : ""}
162163
/>
163164
));
164165
};
@@ -176,14 +177,16 @@ const NightSky = () => {
176177
{ x: 10, y: 40 },
177178
{ x: 90, y: 10 },
178179
];
180+
// Start index after regular stars (40 stars * 3 properties each = 120)
181+
const baseIndex = 120;
179182
return animatedStarPositions.map((pos, i) => (
180183
<circle
181184
key={`animated-${i}`}
182185
cx={pos.x}
183186
cy={pos.y}
184-
r={seededRandom() * 0.2 + 0.1}
187+
r={seededRandom(baseIndex + i * 2) * 0.2 + 0.1}
185188
fill="white"
186-
opacity={seededRandom() * 0.5 + 0.5}
189+
opacity={seededRandom(baseIndex + i * 2 + 1) * 0.5 + 0.5}
187190
className={`gentle-move${(i % 3) + 1}`}
188191
/>
189192
));

0 commit comments

Comments
 (0)