refactor: extract sequence logic and simplify TextMotion conditionals#76
Conversation
📝 WalkthroughWalkthroughTextMotion now branches rendering based on Changes
Sequence Diagram(s)sequenceDiagram
participant TextMotion as TextMotion (component)
participant Hook as useAnimatedChildren (hook)
participant Helpers as sequenceHelpers (utils)
TextMotion->>Hook: call useAnimatedChildren(children, options)
Hook->>Hook: split nodes (splitNodeAndExtractText)
Hook->>Helpers: calculateSequenceIndex(currentIndex, totalNodes, order)
Helpers-->>Hook: sequenceIndex
Hook->>Hook: wrap node with animated span (isLastNode -> mark last)
Hook-->>TextMotion: return animatedChildren (nodes)
TextMotion->>DOM: render animatedChildren or children based on shouldAnimate
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @src/hooks/useAnimatedChildren/useAnimatedChildren.tsx:
- Around line 72-94: The sequenceIndex is being incremented for every node due
to sequenceIndex++ at the top of the map, which consumes indexes for non-leaf
nodes and breaks isLastNode detection; fix by only consuming/incrementing
sequenceIndex when you actually create an AnimatedSpan for a text node: for text
nodes set const currentIndex = sequenceIndex; then increment sequenceIndex++
after creating the AnimatedSpan (or immediately after using currentIndex to
compute calculatedSequenceIndex/isLast and pass it to
AnimatedSpan/onAnimationEnd); for element nodes do not increment beforehand—call
wrapWithAnimatedSpan with the current sequenceIndex and then assign
sequenceIndex = nextSequenceIndex returned from wrapWithAnimatedSpan; keep uses
of calculateSequenceIndex, isLastNode, isTextNode, isElementWithChildren,
wrapWithAnimatedSpan, and AnimatedSpan unchanged otherwise.
🧹 Nitpick comments (2)
src/utils/sequenceHelpers/sequenceHelpers.spec.ts (1)
3-15: Consider adding edge-case tests for boundary conditions.The current tests cover the happy path well. Consider adding tests for:
totalNodes = 1(single element)totalNodes = 0(empty sequence) — what should the behavior be?These edge cases would help document expected behavior and guard against regressions.
src/components/TextMotion/TextMotion.tsx (1)
97-103: Unnecessary computation whenshouldAnimateis false.When
shouldAnimateisfalse, the early return at line 111 renders{children}directly, soanimatedChildrenis never used. However,useAnimatedChildrenis still invoked with[children], performing unnecessary work.Consider moving the hook call inside the animated branch or conditionally skipping the computation:
♻️ Suggested approach
- const animatedChildren = useAnimatedChildren({ - splittedNode: shouldAnimate ? splittedNode : [children], - initialDelay, - animationOrder, - resolvedMotion, - onAnimationEnd, - }); + const animatedChildren = useAnimatedChildren({ + splittedNode, + initialDelay, + animationOrder, + resolvedMotion, + onAnimationEnd, + enabled: shouldAnimate, + });Alternatively, if hook rules prevent conditional calls, consider having
useAnimatedChildrenaccept anenabledflag to skip internal processing when false.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/TextMotion/TextMotion.tsxsrc/hooks/useAnimatedChildren/useAnimatedChildren.tsxsrc/utils/sequenceHelpers/index.tssrc/utils/sequenceHelpers/sequenceHelpers.spec.tssrc/utils/sequenceHelpers/sequenceHelpers.tssrc/utils/typeGuards/typeGuards.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
⚙️ CodeRabbit configuration file
**/*.{ts,tsx}: You are a senior frontend reviewer.
Review this code strictly based on the principles of "easy-to-change code"
from Frontend Fundamentals (Readability, Predictability, Cohesion, Low Coupling).================================
- Readability
================================
- Check if the code can be read top-to-bottom like a document.
- Flag places where the reader must jump around to understand logic.
- Suggest extracting logic when multiple contexts are mixed in one function or component.
- Identify magic numbers or strings and suggest meaningful named constants.
- Point out unclear or abbreviated variable/function names.
- Prefer simple control flow over nested or overly clever logic.
================================
2. Predictability
- Verify that function and variable names clearly imply their behavior.
- Ensure functions with similar responsibilities return consistent data shapes.
- Flag hidden side effects (global state mutation, implicit behavior).
- Warn when the same name is used with different meanings across the codebase.
- Suggest making implicit assumptions explicit.
================================
3. Cohesion
- Check whether code that changes together is located together.
- Flag cases where related logic is scattered across multiple files unnecessarily.
- Suggest grouping constants, helpers, and hooks near the domain they belong to.
- Identify components or functions that contain unrelated responsibilities.
- Prefer feature/domain-based structure over technical grouping when applicable.
================================
4. Coupling
- Identify code where a small change would cause wide ripple effects.
- Warn when abstractions increase coupling instead of reducing it.
- Prefer duplication over premature abstraction if it lowers change impact.
- Flag excessive prop drilling and suggest alternative patterns if appropriate.
- Ensure compo...
Files:
src/utils/typeGuards/typeGuards.tssrc/utils/sequenceHelpers/sequenceHelpers.spec.tssrc/utils/sequenceHelpers/sequenceHelpers.tssrc/utils/sequenceHelpers/index.tssrc/components/TextMotion/TextMotion.tsxsrc/hooks/useAnimatedChildren/useAnimatedChildren.tsx
🧬 Code graph analysis (5)
src/utils/typeGuards/typeGuards.ts (1)
src/utils/typeGuards/typeGuards.spec.ts (6)
describe(5-110)it(70-109)it(6-36)it(38-68)element(76-79)element(28-31)
src/utils/sequenceHelpers/sequenceHelpers.spec.ts (1)
src/utils/sequenceHelpers/sequenceHelpers.ts (2)
calculateSequenceIndex(13-19)isLastNode(30-32)
src/utils/sequenceHelpers/sequenceHelpers.ts (2)
src/types/common.ts (1)
AnimationOrder(35-35)src/utils/generateAnimation/generateAnimation.ts (1)
accumulator(27-54)
src/components/TextMotion/TextMotion.tsx (4)
src/utils/splitNodeAndExtractText/splitNodeAndExtractText.ts (1)
splitNodeAndExtractText(18-55)src/hooks/useResolvedMotion/useResolvedMotion.ts (1)
useResolvedMotion(25-39)src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (1)
useAnimatedChildren(36-59)src/constants/defaults.ts (1)
DEFAULT_ARIA_LABEL(9-9)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (4)
src/types/common.ts (2)
AnimationOrder(35-35)Motion(51-60)src/utils/sequenceHelpers/sequenceHelpers.ts (2)
calculateSequenceIndex(13-19)isLastNode(30-32)src/utils/typeGuards/typeGuards.ts (2)
isTextNode(11-13)isElementWithChildren(35-42)src/utils/generateAnimation/generateAnimation.ts (1)
generateAnimation(21-48)
🔇 Additional comments (12)
src/utils/sequenceHelpers/sequenceHelpers.spec.ts (1)
17-26: LGTM!The
isLastNodetests appropriately cover the true and false branches with representative indices.src/utils/sequenceHelpers/index.ts (1)
1-1: LGTM!Clean barrel export that provides a single entry point for the sequence helper utilities.
src/utils/typeGuards/typeGuards.ts (3)
3-13: LGTM!The updated JSDoc improves clarity by explicitly stating the return semantics. Consistent documentation pattern across all type guards.
15-25: LGTM!Documentation update aligns with the consistent pattern established for type guards.
27-42: LGTM!Documentation accurately describes the function behavior.
src/utils/sequenceHelpers/sequenceHelpers.ts (2)
13-19: LGTM! Clean pure function implementation.The function correctly computes sequence indices for both animation orders. The formula
totalNodes - currentIndex - 1correctly maps the first element (0) to the last position and vice versa.
30-32: LGTM!Simple and predictable. The function name clearly conveys its purpose.
src/components/TextMotion/TextMotion.tsx (2)
111-123: LGTM! Clear separation of rendering paths.The flattened conditional structure improves readability by making the two rendering paths explicit. Both branches correctly:
- Attach
targetReffor intersection observer- Use
text || DEFAULT_ARIA_LABELfor accessibility- Apply distinct CSS classes for styling control
94-94: Acceptable trade-off for unconditional split computation.Always calling
splitNodeAndExtractTextensurestextis available for thearia-labelin both rendering paths. While this means computingsplittedNodeeven when not animating, the consistency and simpler control flow justify the minor overhead.src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (3)
18-21: Good addition of explicit return type.The
WrapResulttype makes the return shape ofwrapWithAnimatedSpanexplicit and predictable. This aligns well with the PR goal of removing implicit behavior.
43-58: Clean refactor removing ref-based state management.The hook now delegates sequence index calculation to
wrapWithAnimatedSpanwith an explicit starting index of0. This makes the flow easier to follow and removes hidden state dependencies. The memoization dependencies are correctly specified.
61-69: Function signature is now explicit and pure.Converting
wrapWithAnimatedSpanto acceptcurrentSequenceIndexand returnWrapResultmakes the data flow explicit. This aligns well with the PR goal of improving predictability by removing implicit state dependencies. The function is now easier to test in isolation.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (1)
62-107: Solid refactor from mutable ref to pure function pattern.Converting
wrapWithAnimatedSpanto return{ nodes, nextSequenceIndex }instead of mutating a ref is a significant predictability improvement. The function's behavior is now self-contained and easier to reason about.A few observations on the implementation:
Local mutation is acceptable here: The
let sequenceIndex(line 71) is scoped within the function and explicitly returned, which is a common and appropriate pattern for accumulator-style logic.Clear sequence propagation: The recursive call pattern (lines 86-95) where
nextSequenceIndexis captured and assigned tosequenceIndexis explicit and traceable.Consider naming for clarity: The parameter
currentSequenceIndexand localsequenceIndexcould be confused. A slight rename likestartIndexfor the parameter might improve readability, but this is a minor nitpick.src/utils/countNodes/countNodes.ts (1)
21-25: Consider renaming for predictability.The logic is correct and well-structured. However, since this function now specifically counts text nodes rather than all nodes, consider whether renaming to
countTextNodesorcountAnimatableNodeswould improve predictability.The current name
countNodescombined with the parameternodescould lead readers to expect it counts all nodes, requiring them to read the JSDoc or implementation to understand the actual behavior.Trade-off: Renaming would improve self-documentation but requires updating all call sites. If the function is only used in animation contexts where "nodes" implicitly means "animatable nodes," keeping the name may be acceptable.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsxsrc/utils/countNodes/countNodes.spec.tsxsrc/utils/countNodes/countNodes.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
⚙️ CodeRabbit configuration file
**/*.{ts,tsx}: You are a senior frontend reviewer.
Review this code strictly based on the principles of "easy-to-change code"
from Frontend Fundamentals (Readability, Predictability, Cohesion, Low Coupling).================================
- Readability
================================
- Check if the code can be read top-to-bottom like a document.
- Flag places where the reader must jump around to understand logic.
- Suggest extracting logic when multiple contexts are mixed in one function or component.
- Identify magic numbers or strings and suggest meaningful named constants.
- Point out unclear or abbreviated variable/function names.
- Prefer simple control flow over nested or overly clever logic.
================================
2. Predictability
- Verify that function and variable names clearly imply their behavior.
- Ensure functions with similar responsibilities return consistent data shapes.
- Flag hidden side effects (global state mutation, implicit behavior).
- Warn when the same name is used with different meanings across the codebase.
- Suggest making implicit assumptions explicit.
================================
3. Cohesion
- Check whether code that changes together is located together.
- Flag cases where related logic is scattered across multiple files unnecessarily.
- Suggest grouping constants, helpers, and hooks near the domain they belong to.
- Identify components or functions that contain unrelated responsibilities.
- Prefer feature/domain-based structure over technical grouping when applicable.
================================
4. Coupling
- Identify code where a small change would cause wide ripple effects.
- Warn when abstractions increase coupling instead of reducing it.
- Prefer duplication over premature abstraction if it lowers change impact.
- Flag excessive prop drilling and suggest alternative patterns if appropriate.
- Ensure compo...
Files:
src/utils/countNodes/countNodes.tssrc/utils/countNodes/countNodes.spec.tsxsrc/hooks/useAnimatedChildren/useAnimatedChildren.tsx
🧬 Code graph analysis (2)
src/utils/countNodes/countNodes.ts (1)
src/utils/typeGuards/typeGuards.ts (2)
isTextNode(11-13)isElementWithChildren(35-42)
src/utils/countNodes/countNodes.spec.tsx (1)
src/utils/countNodes/countNodes.ts (1)
countNodes(13-29)
🔇 Additional comments (7)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (3)
7-8: Good extraction of sequence helpers.The extraction of
calculateSequenceIndexandisLastNodeinto dedicated helpers improves cohesion by grouping sequence-related logic together and makes the animation ordering logic easier to test independently.
18-21: Well-defined return type for clarity.The
WrapResulttype makes the function's contract explicit and improves predictability by clearly communicating that the function returns both the wrapped nodes and the updated sequence index.
46-56: Clean hook implementation with explicit data flow.Destructuring
{ nodes }from the result and passing0as the initial sequence index makes the data flow explicit and easier to trace compared to the previous ref-based approach.src/utils/countNodes/countNodes.ts (1)
7-11: Documentation clearly explains the refined behavior.The updated JSDoc accurately describes that this counts text nodes specifically (the animatable nodes). This makes the function's purpose explicit.
src/utils/countNodes/countNodes.spec.tsx (3)
13-23: Test expectations correctly updated for text-node counting.The distinction between
<>Hello World</>(single text node = 1) and{'Hello'} {'World'}(three separate text nodes including the space = 3) correctly validates the new counting behavior.
87-90: Good edge case: empty array returns 0.The test description update from implicit to explicit "should return 0 for an empty array" improves test readability.
116-132: Comprehensive complex structure test.This test effectively validates the recursive counting through multiple nesting levels. The expected count of 5 correctly accounts for: "Line 1", "Part 1 ", "Part 2", "Code", and " More Code".
Description
This PR refactors focusing on improving predictability and reducing implicit behavior in animation sequencing and rendering logic.
Sequence management improvements
calculateSequenceIndexfor deterministic index calculationisLastNodefor explicit end-of-sequence checkswrapWithAnimatedSpaninto a pure function by removing side effects and implicit state dependencies.Conditional logic simplification
TextMotionby flattening nested conditions and making rendering paths explicit.Type of change
Checklist
Summary by CodeRabbit
Bug Fixes
New Features
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.