Skip to content

refactor: extract animation logics into hooks#77

Merged
shubug1015 merged 3 commits intomainfrom
refactor/component-separation
Jan 13, 2026
Merged

refactor: extract animation logics into hooks#77
shubug1015 merged 3 commits intomainfrom
refactor/component-separation

Conversation

@shubug1015
Copy link
Copy Markdown
Owner

@shubug1015 shubug1015 commented Jan 13, 2026

Description

This PR refactors focusing on improving component cohesion by clearly separating rendering responsibilities from animation-related logic.

  1. TextMotion responsibility separation

    • Extracted animation-related logic from TextMotion into a dedicated hook, useTextMotionAnimation.
    • TextMotion is now primarily responsible for rendering, while animation state and behavior are handled by the hook.
    • This results in a simpler component structure and improved readability.
  2. AnimatedSpan side-effect isolation

    • Extracted duplicate animation end–handling logic into a reusable hook, useAnimationEndCallback.
    • Centralized the logic that prevents redundant animation end callbacks.
    • This reduces duplication and ensures consistent behavior across animation-related components.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor

Checklist

  • My code follows the project style guidelines
  • I performed a self-review of my own code
  • I added tests that prove my fix is effective
  • I added necessary documentation

Summary by CodeRabbit

Release Notes

  • Tests

    • Expanded test coverage for animation behavior and edge cases.
  • Refactor

    • Improved internal animation handling efficiency through code reorganization.

✏️ Tip: You can customize this high-level summary in your review settings.

@shubug1015 shubug1015 requested a review from kjyong702 January 13, 2026 07:40
@shubug1015 shubug1015 self-assigned this Jan 13, 2026
@shubug1015 shubug1015 added the refactor Refactor code label Jan 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

The PR refactors the TextMotion component architecture by introducing a new useTextMotionAnimation hook that consolidates animation orchestration logic, moves AnimatedSpan to the hooks module, and creates a reusable useAnimationEndCallback hook for single-invocation animation handlers.

Changes

Cohort / File(s) Summary
Hook consolidation
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts, src/hooks/useTextMotionAnimation/useTextMotionAnimation.spec.tsx, src/hooks/useTextMotionAnimation/index.ts
New hook that orchestrates text motion animations by composing useIntersectionObserver, useResolvedMotion, and useAnimatedChildren; includes comprehensive test coverage for trigger types, motion resolution, and animation child generation.
Animation callback abstraction
src/hooks/useAnimationEndCallback/useAnimationEndCallback.ts, src/hooks/useAnimationEndCallback/useAnimationEndCallback.spec.tsx, src/hooks/useAnimationEndCallback/index.ts
New hook providing single-invocation animation end handlers; includes tests validating callback invocation behavior and dynamic callback updates.
TextMotion component refactoring
src/components/TextMotion/TextMotion.tsx, src/components/TextMotion/TextMotion.spec.tsx
Component simplified to rely on useTextMotionAnimation instead of multiple internal hooks; test suite rewritten to mock the new hook and verify hook-driven state (shouldAnimate, animatedChildren).
AnimatedSpan relocation
src/components/AnimatedSpan/index.ts, src/hooks/useAnimatedChildren/AnimatedSpan.tsx, src/hooks/useAnimatedChildren/useAnimatedChildren.tsx, src/hooks/useAnimatedChildren/index.ts
AnimatedSpan removed from component barrel export; moved to hooks module with useAnimationEndCallback integration; useAnimatedChildren import path updated.
Test coverage expansion
src/utils/countNodes/countNodes.spec.tsx
Added test case for null node handling in countNodes utility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • kjyong702
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main refactoring objective of extracting animation-related logic into reusable hooks.
Description check ✅ Passed The description clearly explains the refactoring objectives, includes a complete checklist with appropriate selections, and covers all required sections from the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Jan 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/hooks/useAnimationEndCallback/useAnimationEndCallback.ts (1)

11-22: Implementation correctly guards against multiple invocations, but consider documenting the non-resetting behavior.

The calledRef is never reset, even when the callback prop changes. This means:

  • First callback change + invocation → callback fires
  • Subsequent invocations → no callback fires (even if callback identity changed)

This is likely correct for animation end handling where you want exactly one notification per component lifecycle, but consider adding a brief comment or updating the JSDoc to clarify this contract.

📝 Suggested documentation improvement
 /**
  * @description
  * `useAnimationEndCallback` is a custom hook that calls a callback function when the animation ends.
  * It returns a function that should be passed to the `onAnimationEnd` prop of the animated component.
+ * The callback is invoked at most once per component mount, regardless of callback identity changes.
  *
  * @param {() => void} callback - The callback function to call when the animation ends.
  * @returns {() => void} A function that should be passed to the `onAnimationEnd` prop of the animated component.
  */
src/components/TextMotion/TextMotion.spec.tsx (3)

65-106: Duplicate test cases for shouldAnimate: true rendering.

Lines 65-79 and 92-106 test the same behavior with nearly identical implementations:

  • Both set shouldAnimate: true
  • Both create the same animatedChildren structure
  • Both assert on span count and text-motion class

Consider consolidating these into a single test or differentiating their purposes (e.g., one tests trigger="on-load", another tests intersection-based activation) by varying the mock setup more meaningfully.


143-177: Duplicate test cases for onAnimationStart callback.

Lines 143-153 and 167-177 are effectively identical tests:

  • Both set shouldAnimate: true
  • Both assert onAnimationStart was called once

The comment on line 167 says "(e.g., intersecting)" but the mock setup is the same as line 147. If these are meant to test different scenarios, consider making the distinction explicit in the test setup or combining them.


220-240: Remove or address commented-out test.

The commented-out test for line split with <br> elements should either be:

  1. Removed if no longer relevant to the hook-based architecture
  2. Uncommented and fixed if it's still needed
  3. Replaced with a TODO comment explaining why it's disabled and when it will be addressed

Leaving commented-out tests without explanation creates maintenance burden and confusion about test coverage.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0102c0c and 557877b.

📒 Files selected for processing (14)
  • src/components/AnimatedSpan/index.ts
  • src/components/TextMotion/TextMotion.spec.tsx
  • src/components/TextMotion/TextMotion.tsx
  • src/hooks/useAnimatedChildren/AnimatedSpan.spec.tsx
  • src/hooks/useAnimatedChildren/AnimatedSpan.tsx
  • src/hooks/useAnimatedChildren/index.ts
  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
  • src/hooks/useAnimationEndCallback/index.ts
  • src/hooks/useAnimationEndCallback/useAnimationEndCallback.spec.tsx
  • src/hooks/useAnimationEndCallback/useAnimationEndCallback.ts
  • src/hooks/useTextMotionAnimation/index.ts
  • src/hooks/useTextMotionAnimation/useTextMotionAnimation.spec.tsx
  • src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts
  • src/utils/countNodes/countNodes.spec.tsx
💤 Files with no reviewable changes (1)
  • src/components/AnimatedSpan/index.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).

================================

  1. 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/hooks/useAnimatedChildren/AnimatedSpan.tsx
  • src/hooks/useAnimatedChildren/index.ts
  • src/hooks/useAnimationEndCallback/index.ts
  • src/hooks/useTextMotionAnimation/index.ts
  • src/hooks/useAnimationEndCallback/useAnimationEndCallback.ts
  • src/hooks/useAnimationEndCallback/useAnimationEndCallback.spec.tsx
  • src/hooks/useTextMotionAnimation/useTextMotionAnimation.spec.tsx
  • src/components/TextMotion/TextMotion.spec.tsx
  • src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts
  • src/hooks/useAnimatedChildren/useAnimatedChildren.tsx
  • src/utils/countNodes/countNodes.spec.tsx
  • src/components/TextMotion/TextMotion.tsx
🧬 Code graph analysis (5)
src/hooks/useTextMotionAnimation/useTextMotionAnimation.spec.tsx (3)
src/types/props.ts (1)
  • TextMotionProps (22-22)
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts (1)
  • useTextMotionAnimation (17-51)
src/types/common.ts (1)
  • Preset (67-83)
src/components/TextMotion/TextMotion.spec.tsx (3)
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts (1)
  • useTextMotionAnimation (17-51)
src/components/TextMotion/TextMotion.tsx (1)
  • TextMotion (70-96)
src/constants/defaults.ts (1)
  • DEFAULT_ARIA_LABEL (9-9)
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts (3)
src/hooks/useTextMotionAnimation/index.ts (1)
  • useTextMotionAnimation (1-1)
src/types/props.ts (1)
  • TextMotionProps (22-22)
src/hooks/useIntersectionObserver/useIntersectionObserver.spec.tsx (1)
  • trigger (26-43)
src/utils/countNodes/countNodes.spec.tsx (1)
src/utils/countNodes/countNodes.ts (1)
  • countNodes (13-29)
src/components/TextMotion/TextMotion.tsx (3)
src/hooks/useValidation/useValidation.ts (1)
  • useValidation (20-28)
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts (1)
  • useTextMotionAnimation (17-51)
src/constants/defaults.ts (1)
  • DEFAULT_ARIA_LABEL (9-9)
🔇 Additional comments (11)
src/utils/countNodes/countNodes.spec.tsx (1)

8-11: LGTM! Good edge case coverage.

This test complements the existing test at lines 97-107 by testing direct null array input without the JSX/getNodes wrapper. This ensures countNodes handles raw null values correctly at the array level, not just nulls filtered through React's Children.toArray.

src/hooks/useAnimatedChildren/useAnimatedChildren.tsx (1)

9-9: LGTM! Good cohesion improvement.

Moving AnimatedSpan closer to its primary consumer (useAnimatedChildren) follows the principle of colocating code that changes together. This reduces cross-directory coupling and makes the module boundary clearer.

src/hooks/useAnimationEndCallback/index.ts (1)

1-1: LGTM!

Standard barrel export pattern consistent with other hook modules in the codebase.

src/hooks/useAnimatedChildren/index.ts (1)

1-2: LGTM! Reasonable colocation trade-off.

Exporting a component (AnimatedSpan) from a hooks directory is a slight naming inconsistency, but the cohesion benefit outweighs this concern since AnimatedSpan is tightly coupled to useAnimatedChildren. Keeping implementation details near their primary consumer improves maintainability.

src/hooks/useTextMotionAnimation/index.ts (1)

1-1: LGTM!

Standard barrel export following the established pattern for hook modules.

src/hooks/useAnimatedChildren/AnimatedSpan.tsx (1)

21-32: Clean refactor delegating animation end handling to the dedicated hook.

The component now focuses solely on rendering while the useAnimationEndCallback hook encapsulates the single-invocation guard logic. This improves cohesion by keeping animation end behavior centralized and reusable.

src/hooks/useAnimationEndCallback/useAnimationEndCallback.spec.tsx (1)

34-56: Test coverage is comprehensive, but verify the intended behavior on callback change.

The test correctly validates that when the callback changes:

  1. The old callback is not invoked
  2. The new callback is invoked once
  3. Subsequent invocations do not call the new callback again

This means the "called once" guard persists across callback changes. If the intent is to allow each new callback to be invoked once (resetting the guard on change), the hook implementation would need adjustment.

Please confirm: Is the current behavior intentional where changing the callback does NOT reset the single-invocation guard? If a component re-renders with a new onAnimationEnd callback mid-animation, it would still only fire once total—not once per unique callback.

src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts (1)

17-51: Well-structured orchestration hook that cleanly composes animation logic.

The hook successfully centralizes the animation orchestration that was previously scattered in TextMotion:

  • Cohesion: Related animation state (shouldAnimate, targetRef, animatedChildren, text) is computed and returned together
  • Predictability: Clear derivation of shouldAnimate from trigger and intersection state
  • Low coupling: TextMotion now depends only on this hook's return value, not on multiple individual hooks

The conditional splittedNode on line 38 is a nice touch—it ensures useAnimatedChildren always receives valid input while respecting hook call order rules.

src/hooks/useTextMotionAnimation/useTextMotionAnimation.spec.tsx (1)

16-108: Comprehensive test coverage for the orchestration hook.

The test suite effectively validates:

  1. Correct argument propagation to dependencies (splitNodeAndExtractText, useResolvedMotion, useAnimatedChildren)
  2. shouldAnimate derivation across trigger/intersection combinations
  3. Conditional splittedNode behavior (split nodes when animating, original children when not)
  4. Return value composition

The mocking strategy appropriately isolates the hook's orchestration logic from its dependencies.

src/components/TextMotion/TextMotion.spec.tsx (1)

18-33: Good pattern: MockTextMotion helper for driving hook-based test scenarios.

The helper cleanly encapsulates the mock setup, making individual tests more readable and focused on the scenario being tested rather than mock boilerplate.

src/components/TextMotion/TextMotion.tsx (1)

70-95: No breaking change — TextMotionProps intentionally restricts accepted props via TypeScript type system.

The component correctly implements its documented interface. TextMotionProps is explicitly defined in the type system and does not extend HTML element attributes. Consumers cannot pass data-testid, className, style, or other HTML attributes to TextMotion — TypeScript will error if they attempt to do so. This is intentional design, not a silent behavior change.

@shubug1015 shubug1015 marked this pull request as ready for review January 13, 2026 07:52
@shubug1015 shubug1015 merged commit c5e57ea into main Jan 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Refactor code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant