From 6016e09b3cb7d4645be3ba6abc338a033c613596 Mon Sep 17 00:00:00 2001 From: zbysir <61535029+zbysir@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:33:39 +0800 Subject: [PATCH] fix: guard render-time window/matchMedia access for SSR DecayCard, PixelCard and Masonry read browser globals during render (in useRef/useState initializers), which throws "window is not defined" or "matchMedia is not defined" under SSR (e.g. Next.js). Guard each with a typeof check, matching the pattern used in #911. - DecayCard: guard window.innerWidth/innerHeight in the cursor/winsize useRef initializers (fallback 0) - PixelCard: guard window.matchMedia in the reducedMotion useRef initializer (fallback false) - Masonry: guard matchMedia in the useMedia `get` initializer consumed by useState (fallback defaultValue) Effect/handler code is unchanged. Updated across all 4 variants of each component per CONTRIBUTING. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/content/Components/DecayCard/DecayCard.jsx | 10 ++++++++-- src/content/Components/Masonry/Masonry.jsx | 5 ++++- src/content/Components/PixelCard/PixelCard.jsx | 4 +++- src/tailwind/Components/DecayCard/DecayCard.jsx | 10 ++++++++-- src/tailwind/Components/Masonry/Masonry.jsx | 5 ++++- src/tailwind/Components/PixelCard/PixelCard.jsx | 4 +++- src/ts-default/Components/DecayCard/DecayCard.tsx | 8 ++++---- src/ts-default/Components/Masonry/Masonry.tsx | 5 ++++- src/ts-default/Components/PixelCard/PixelCard.tsx | 4 +++- src/ts-tailwind/Components/DecayCard/DecayCard.tsx | 8 ++++---- src/ts-tailwind/Components/Masonry/Masonry.tsx | 5 ++++- src/ts-tailwind/Components/PixelCard/PixelCard.tsx | 4 +++- 12 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/content/Components/DecayCard/DecayCard.jsx b/src/content/Components/DecayCard/DecayCard.jsx index a3e7b1e02..2b5c9d954 100644 --- a/src/content/Components/DecayCard/DecayCard.jsx +++ b/src/content/Components/DecayCard/DecayCard.jsx @@ -16,9 +16,15 @@ const DecayCard = ({ }) => { const svgRef = useRef(null); const displacementMapRef = useRef(null); - const cursor = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); + const cursor = useRef({ + x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0, + y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0 + }); const cachedCursor = useRef({ ...cursor.current }); - const winsize = useRef({ width: window.innerWidth, height: window.innerHeight }); + const winsize = useRef({ + width: typeof window !== 'undefined' ? window.innerWidth : 0, + height: typeof window !== 'undefined' ? window.innerHeight : 0 + }); useEffect(() => { const lerp = (a, b, n) => (1 - n) * a + n * b; diff --git a/src/content/Components/Masonry/Masonry.jsx b/src/content/Components/Masonry/Masonry.jsx index 3340a13b0..7b4097cd3 100644 --- a/src/content/Components/Masonry/Masonry.jsx +++ b/src/content/Components/Masonry/Masonry.jsx @@ -4,7 +4,10 @@ import { gsap } from 'gsap'; import './Masonry.css'; const useMedia = (queries, values, defaultValue) => { - const get = () => values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + const get = () => { + if (typeof window === 'undefined') return defaultValue; + return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + }; const [value, setValue] = useState(get); diff --git a/src/content/Components/PixelCard/PixelCard.jsx b/src/content/Components/PixelCard/PixelCard.jsx index fb820cfe4..09320498c 100644 --- a/src/content/Components/PixelCard/PixelCard.jsx +++ b/src/content/Components/PixelCard/PixelCard.jsx @@ -128,7 +128,9 @@ export default function PixelCard({ variant = 'default', gap, speed, colors, noF const pixelsRef = useRef([]); const animationRef = useRef(null); const timePreviousRef = useRef(performance.now()); - const reducedMotion = useRef(window.matchMedia('(prefers-reduced-motion: reduce)').matches).current; + const reducedMotion = useRef( + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current; const variantCfg = VARIANTS[variant] || VARIANTS.default; const finalGap = gap ?? variantCfg.gap; diff --git a/src/tailwind/Components/DecayCard/DecayCard.jsx b/src/tailwind/Components/DecayCard/DecayCard.jsx index 14dac0472..5a511f350 100644 --- a/src/tailwind/Components/DecayCard/DecayCard.jsx +++ b/src/tailwind/Components/DecayCard/DecayCard.jsx @@ -14,9 +14,15 @@ const DecayCard = ({ }) => { const svgRef = useRef(null); const displacementMapRef = useRef(null); - const cursor = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); + const cursor = useRef({ + x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0, + y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0 + }); const cachedCursor = useRef({ ...cursor.current }); - const winsize = useRef({ width: window.innerWidth, height: window.innerHeight }); + const winsize = useRef({ + width: typeof window !== 'undefined' ? window.innerWidth : 0, + height: typeof window !== 'undefined' ? window.innerHeight : 0 + }); useEffect(() => { const lerp = (a, b, n) => (1 - n) * a + n * b; diff --git a/src/tailwind/Components/Masonry/Masonry.jsx b/src/tailwind/Components/Masonry/Masonry.jsx index 427248f8a..8f47c4606 100644 --- a/src/tailwind/Components/Masonry/Masonry.jsx +++ b/src/tailwind/Components/Masonry/Masonry.jsx @@ -2,7 +2,10 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { gsap } from 'gsap'; const useMedia = (queries, values, defaultValue) => { - const get = () => values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + const get = () => { + if (typeof window === 'undefined') return defaultValue; + return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + }; const [value, setValue] = useState(get); diff --git a/src/tailwind/Components/PixelCard/PixelCard.jsx b/src/tailwind/Components/PixelCard/PixelCard.jsx index 5a741c9bd..793587a5a 100644 --- a/src/tailwind/Components/PixelCard/PixelCard.jsx +++ b/src/tailwind/Components/PixelCard/PixelCard.jsx @@ -127,7 +127,9 @@ export default function PixelCard({ variant = 'default', gap, speed, colors, noF const pixelsRef = useRef([]); const animationRef = useRef(null); const timePreviousRef = useRef(performance.now()); - const reducedMotion = useRef(window.matchMedia('(prefers-reduced-motion: reduce)').matches).current; + const reducedMotion = useRef( + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current; const variantCfg = VARIANTS[variant] || VARIANTS.default; const finalGap = gap ?? variantCfg.gap; diff --git a/src/ts-default/Components/DecayCard/DecayCard.tsx b/src/ts-default/Components/DecayCard/DecayCard.tsx index 68621f60e..3e4517cc6 100644 --- a/src/ts-default/Components/DecayCard/DecayCard.tsx +++ b/src/ts-default/Components/DecayCard/DecayCard.tsx @@ -28,13 +28,13 @@ const DecayCard: React.FC = ({ const svgRef = useRef(null); const displacementMapRef = useRef(null); const cursor = useRef<{ x: number; y: number }>({ - x: window.innerWidth / 2, - y: window.innerHeight / 2 + x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0, + y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0 }); const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current }); const winsize = useRef<{ width: number; height: number }>({ - width: window.innerWidth, - height: window.innerHeight + width: typeof window !== 'undefined' ? window.innerWidth : 0, + height: typeof window !== 'undefined' ? window.innerHeight : 0 }); useEffect(() => { diff --git a/src/ts-default/Components/Masonry/Masonry.tsx b/src/ts-default/Components/Masonry/Masonry.tsx index ed268b0e3..c034e762e 100644 --- a/src/ts-default/Components/Masonry/Masonry.tsx +++ b/src/ts-default/Components/Masonry/Masonry.tsx @@ -4,7 +4,10 @@ import { gsap } from 'gsap'; import './Masonry.css'; const useMedia = (queries: string[], values: number[], defaultValue: number): number => { - const get = () => values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + const get = () => { + if (typeof window === 'undefined') return defaultValue; + return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + }; const [value, setValue] = useState(get); diff --git a/src/ts-default/Components/PixelCard/PixelCard.tsx b/src/ts-default/Components/PixelCard/PixelCard.tsx index 78729ee15..ae4aa6a0d 100644 --- a/src/ts-default/Components/PixelCard/PixelCard.tsx +++ b/src/ts-default/Components/PixelCard/PixelCard.tsx @@ -181,7 +181,9 @@ export default function PixelCard({ const pixelsRef = useRef([]); const animationRef = useRef | null>(null); const timePreviousRef = useRef(performance.now()); - const reducedMotion = useRef(window.matchMedia('(prefers-reduced-motion: reduce)').matches).current; + const reducedMotion = useRef( + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current; const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default; const finalGap = gap ?? variantCfg.gap; diff --git a/src/ts-tailwind/Components/DecayCard/DecayCard.tsx b/src/ts-tailwind/Components/DecayCard/DecayCard.tsx index 3a70f9bcb..2dbd2e2a3 100644 --- a/src/ts-tailwind/Components/DecayCard/DecayCard.tsx +++ b/src/ts-tailwind/Components/DecayCard/DecayCard.tsx @@ -27,13 +27,13 @@ const DecayCard: React.FC = ({ const svgRef = useRef(null); const displacementMapRef = useRef(null); const cursor = useRef<{ x: number; y: number }>({ - x: window.innerWidth / 2, - y: window.innerHeight / 2 + x: typeof window !== 'undefined' ? window.innerWidth / 2 : 0, + y: typeof window !== 'undefined' ? window.innerHeight / 2 : 0 }); const cachedCursor = useRef<{ x: number; y: number }>({ ...cursor.current }); const winsize = useRef<{ width: number; height: number }>({ - width: window.innerWidth, - height: window.innerHeight + width: typeof window !== 'undefined' ? window.innerWidth : 0, + height: typeof window !== 'undefined' ? window.innerHeight : 0 }); useEffect(() => { diff --git a/src/ts-tailwind/Components/Masonry/Masonry.tsx b/src/ts-tailwind/Components/Masonry/Masonry.tsx index 487949cdc..17ace5cf9 100644 --- a/src/ts-tailwind/Components/Masonry/Masonry.tsx +++ b/src/ts-tailwind/Components/Masonry/Masonry.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're import { gsap } from 'gsap'; const useMedia = (queries: string[], values: number[], defaultValue: number): number => { - const get = () => values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + const get = () => { + if (typeof window === 'undefined') return defaultValue; + return values[queries.findIndex(q => matchMedia(q).matches)] ?? defaultValue; + }; const [value, setValue] = useState(get); diff --git a/src/ts-tailwind/Components/PixelCard/PixelCard.tsx b/src/ts-tailwind/Components/PixelCard/PixelCard.tsx index 4d9b973d8..fa6402a19 100644 --- a/src/ts-tailwind/Components/PixelCard/PixelCard.tsx +++ b/src/ts-tailwind/Components/PixelCard/PixelCard.tsx @@ -180,7 +180,9 @@ export default function PixelCard({ const pixelsRef = useRef([]); const animationRef = useRef | null>(null); const timePreviousRef = useRef(performance.now()); - const reducedMotion = useRef(window.matchMedia('(prefers-reduced-motion: reduce)').matches).current; + const reducedMotion = useRef( + typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches + ).current; const variantCfg: VariantConfig = VARIANTS[variant] || VARIANTS.default; const finalGap = gap ?? variantCfg.gap;