From 5e6606fc1fe4441cb6e770f89c1bbd23870a440c Mon Sep 17 00:00:00 2001 From: gsmt Date: Tue, 12 May 2026 00:26:28 +0200 Subject: [PATCH 1/2] fix: auto memoization --- packages/calligraph/src/index.tsx | 37 +++--- packages/calligraph/src/number.tsx | 181 +++++++++++++---------------- packages/calligraph/src/slots.tsx | 110 +++++++----------- packages/calligraph/src/text.tsx | 118 ++++++++----------- 4 files changed, 194 insertions(+), 252 deletions(-) diff --git a/packages/calligraph/src/index.tsx b/packages/calligraph/src/index.tsx index faba526..b32477d 100644 --- a/packages/calligraph/src/index.tsx +++ b/packages/calligraph/src/index.tsx @@ -105,7 +105,6 @@ export function Calligraph(props: CalligraphProps) { initial: animateInitial = false, onComplete, autoSize = true, - className, style, ...rest } = props; @@ -115,24 +114,18 @@ export function Calligraph(props: CalligraphProps) { const rendererProps = { text: String(children ?? ""), - Component, transition, stagger, animateInitial, onComplete, - className, - style, - rest, }; - let content: React.ReactNode; - - if (variant === "number") { - content = ; - } else if (variant === "slots") { - content = ; - } else { - content = ( + const content: React.ReactNode = + variant === "number" ? ( + + ) : variant === "slots" ? ( + + ) : ( ); - } - - if (autoSize) { - return {content}; - } - return content; + return ( + + {autoSize ? ( + {content} + ) : ( + content + )} + + ); } diff --git a/packages/calligraph/src/number.tsx b/packages/calligraph/src/number.tsx index 4f69dd7..0b4255d 100644 --- a/packages/calligraph/src/number.tsx +++ b/packages/calligraph/src/number.tsx @@ -1,53 +1,43 @@ import type { Transition } from "motion/react"; import { AnimatePresence, MotionConfig, motion } from "motion/react"; -import { useRef, useState } from "react"; +import { useState } from "react"; import { reconcileDigitKeys } from "./reconcile"; import { DIGIT_DISTANCE, isDigit, splitGraphemes } from "./shared"; export function NumberRenderer({ text, - Component, transition, stagger, animateInitial, onComplete, - className, - style, - rest, }: { text: string; - Component: React.ElementType; transition: Transition; stagger: number; animateInitial: boolean; onComplete?: () => void; - className?: string; - style?: React.CSSProperties; - rest: Record; }) { const chars = splitGraphemes(text); - const nextIdRef = useRef(chars.length); + const [nextId, setNextId] = useState(chars.length); const [prevText, setPrevText] = useState(text); const [digitKeys, setDigitKeys] = useState(() => chars.map((_, i) => i), ); - const dirRef = useRef(1); + const [direction, setDirection] = useState(1); if (text !== prevText) { - const result = reconcileDigitKeys( - prevText, - text, - digitKeys, - nextIdRef.current, + const result = reconcileDigitKeys(prevText, + text, + digitKeys, + nextId ); - nextIdRef.current = result.nextId; - dirRef.current = result.direction; + setNextId(result.nextId); + setDirection(result.direction); setDigitKeys(result.keys); setPrevText(text); } - const dir = dirRef.current; const prefixLen = (() => { const idx = chars.findIndex((c) => isDigit(c)); return idx === -1 ? chars.length : idx; @@ -55,88 +45,81 @@ export function NumberRenderer({ return ( - - - {chars.map((char, i) => { - const isPrefix = i < prefixLen; - const outerKey = isPrefix - ? `pre-${i}` - : `col-${chars.length - 1 - i}`; - const delay = i * stagger; - const isLast = i === chars.length - 1; + + {chars.map((char, i) => { + const isPrefix = i < prefixLen; + const outerKey = isPrefix + ? `pre-${i}` + : `col-${chars.length - 1 - i}`; + const delay = i * stagger; + const isLast = i === chars.length - 1; - return ( - - {isPrefix ? ( - - {char} - - ) : ( - + {isPrefix ? ( + + {char} + + ) : ( + + - )} - - ); - })} - - + {char} + + + )} + + ); + })} + ); } diff --git a/packages/calligraph/src/slots.tsx b/packages/calligraph/src/slots.tsx index f1809aa..caaedea 100644 --- a/packages/calligraph/src/slots.tsx +++ b/packages/calligraph/src/slots.tsx @@ -132,23 +132,15 @@ function SlotColumn({ export function SlotsRenderer({ text, - Component, transition, stagger, animateInitial, - className, - style, - rest, }: { text: string; - Component: React.ElementType; transition: Transition; stagger: number; animateInitial: boolean; onComplete?: () => void; - className?: string; - style?: React.CSSProperties; - rest: Record; }) { const chars = splitGraphemes(text); @@ -157,7 +149,7 @@ export function SlotsRenderer({ const [digitKeys, setDigitKeys] = useState(() => chars.map((_, i) => i), ); - const dirRef = useRef(1); + const [direction, setDirection] = useState(1); const mountedRef = useRef(false); useEffect(() => { @@ -172,12 +164,11 @@ export function SlotsRenderer({ nextIdRef.current, ); nextIdRef.current = result.nextId; - dirRef.current = result.direction; + setDirection(result.direction); setDigitKeys(result.keys); setPrevText(text); } - const dir = dirRef.current; const prefixLen = (() => { const idx = chars.findIndex((c) => isDigit(c)); return idx === -1 ? chars.length : idx; @@ -188,72 +179,61 @@ export function SlotsRenderer({ return ( - - - - {chars.map((char, i) => { - const isPrefix = i < prefixLen; - const outerKey = isPrefix - ? `pre-${i}` - : `col-${chars.length - 1 - i}`; - - if (isPrefix || !isDigit(char)) { - return ( - - {char} - - ); - } - - const delay = (digitCount - 1 - digitIndex) * stagger; - digitIndex++; - + + {chars.map((char, i) => { + const isPrefix = i < prefixLen; + const outerKey = isPrefix + ? `pre-${i}` + : `col-${chars.length - 1 - i}`; + + if (isPrefix || !isDigit(char)) { return ( - + {char} ); - })} - - - + } + + const delay = (digitCount - 1 - digitIndex) * stagger; + digitIndex++; + + return ( + + + + ); + })} + + ); } diff --git a/packages/calligraph/src/text.tsx b/packages/calligraph/src/text.tsx index 5c70cda..8d92588 100644 --- a/packages/calligraph/src/text.tsx +++ b/packages/calligraph/src/text.tsx @@ -1,24 +1,19 @@ import type { Transition } from "motion/react"; import { AnimatePresence, MotionConfig, motion } from "motion/react"; -import { useRef, useState } from "react"; +import { useState } from "react"; import { reconcileTextKeys } from "./reconcile"; import { splitGraphemes } from "./shared"; export function TextRenderer({ text, - Component, transition, driftX, driftY, trend, animateInitial, onComplete, - className, - style, - rest, }: { text: string; - Component: React.ElementType; transition: Transition; driftX: number; driftY: number; @@ -26,12 +21,9 @@ export function TextRenderer({ stagger: number; animateInitial: boolean; onComplete?: () => void; - className?: string; - style?: React.CSSProperties; - rest: Record; }) { const graphemes = splitGraphemes(text); - const nextIdRef = useRef(graphemes.length); + const [nextId, setNextId] = useState(graphemes.length); const [prevText, setPrevText] = useState(text); const [charKeys, setCharKeys] = useState(() => graphemes.map((_, i) => `c${i}`), @@ -39,13 +31,8 @@ export function TextRenderer({ const [changeRatio, setChangeRatio] = useState(0); if (text !== prevText) { - const result = reconcileTextKeys( - prevText, - text, - charKeys, - nextIdRef.current, - ); - nextIdRef.current = result.nextId; + const result = reconcileTextKeys(prevText, text, charKeys, nextId); + setNextId(result.nextId); setPrevText(text); setCharKeys(result.keys); setChangeRatio(result.changeRatio); @@ -53,59 +40,52 @@ export function TextRenderer({ return ( - - - {graphemes.map((char, i) => { - const key = charKeys[i]; - const progress = - graphemes.length <= 1 ? 0 : i / (graphemes.length - 1); - const offsetX = (progress - 0.5) * driftX * changeRatio; - const offsetY = (progress - 0.5) * driftY * changeRatio; - const trendY = trend * 8 * changeRatio; - const isLast = i === graphemes.length - 1; + + {graphemes.map((char, i) => { + const key = charKeys[i]; + const progress = + graphemes.length <= 1 ? 0 : i / (graphemes.length - 1); + const offsetX = (progress - 0.5) * driftX * changeRatio; + const offsetY = (progress - 0.5) * driftY * changeRatio; + const trendY = trend * 8 * changeRatio; + const isLast = i === graphemes.length - 1; - return ( - - ); - })} - - + return ( + + ); + })} + ); } From 7e25880ea061f8516a9002f4c577b6cb031e1976 Mon Sep 17 00:00:00 2001 From: gsmt Date: Tue, 12 May 2026 01:06:41 +0200 Subject: [PATCH 2/2] fix: slots renderer --- packages/calligraph/src/number.tsx | 6 +----- packages/calligraph/src/slots.tsx | 17 ++++++----------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/calligraph/src/number.tsx b/packages/calligraph/src/number.tsx index 0b4255d..00e634b 100644 --- a/packages/calligraph/src/number.tsx +++ b/packages/calligraph/src/number.tsx @@ -27,11 +27,7 @@ export function NumberRenderer({ const [direction, setDirection] = useState(1); if (text !== prevText) { - const result = reconcileDigitKeys(prevText, - text, - digitKeys, - nextId - ); + const result = reconcileDigitKeys(prevText, text, digitKeys, nextId); setNextId(result.nextId); setDirection(result.direction); setDigitKeys(result.keys); diff --git a/packages/calligraph/src/slots.tsx b/packages/calligraph/src/slots.tsx index caaedea..45c0f11 100644 --- a/packages/calligraph/src/slots.tsx +++ b/packages/calligraph/src/slots.tsx @@ -144,26 +144,21 @@ export function SlotsRenderer({ }) { const chars = splitGraphemes(text); - const nextIdRef = useRef(chars.length); + const [nextId, setNextId] = useState(chars.length); const [prevText, setPrevText] = useState(text); const [digitKeys, setDigitKeys] = useState(() => chars.map((_, i) => i), ); const [direction, setDirection] = useState(1); - const mountedRef = useRef(false); + const [isMounted, setIsMounted] = useState(false); useEffect(() => { - mountedRef.current = true; + setIsMounted(true); }, []); if (text !== prevText) { - const result = reconcileDigitKeys( - prevText, - text, - digitKeys, - nextIdRef.current, - ); - nextIdRef.current = result.nextId; + const result = reconcileDigitKeys(prevText, text, digitKeys, nextId); + setNextId(result.nextId); setDirection(result.direction); setDigitKeys(result.keys); setPrevText(text); @@ -227,7 +222,7 @@ export function SlotsRenderer({ direction={direction} transition={transition} delay={delay} - animateIn={mountedRef.current || animateInitial} + animateIn={isMounted || animateInitial} /> );