Skip to content

refactor: improve custom hooks#81

Merged
shubug1015 merged 7 commits intomainfrom
refactor/improve-custom-hooks
Jan 18, 2026
Merged

refactor: improve custom hooks#81
shubug1015 merged 7 commits intomainfrom
refactor/improve-custom-hooks

Conversation

@shubug1015
Copy link
Copy Markdown
Owner

@shubug1015 shubug1015 commented Jan 18, 2026

Description

This PR refactors several custom hooks to improve readability, responsibility separation, and long-term maintainability.

Key improvements include:

  • Clearer hook responsibilities and naming
  • Reduced internal complexity and duplication
  • Improved consistency in hook structure and side-effect handling
  • Safer and more predictable behavior through explicit data flow

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

  • New Features

    • Smoother, more predictable animation sequencing and start behavior.
  • Bug Fixes

    • More reliable visibility detection for consistent animation triggering.
    • Rendering and CSS adjustments for correct active/inactive states.
  • Breaking Changes

    • Inactive CSS class name changed.
    • Newlines no longer auto-convert to line breaks.
    • Developer-facing animation APIs changed and may require consumer updates.
  • Tests

    • Expanded and reorganized tests for animation sequencing and prop validation.

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

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

coderabbitai bot commented Jan 18, 2026

📝 Walkthrough

Walkthrough

Adds a new orchestrator hook useController, replaces several hooks (renames/removes), refactors intersection observer to use a callback ref, centralizes validation into validateProps, introduces useAnimateChildren, updates TextMotion to consume the new controller, and adjusts related tests, exports, and styles.

Changes

Cohort / File(s) Change Summary
Controller Hook
src/hooks/useController/useController.ts, src/hooks/useController/useController.spec.tsx, src/hooks/useController/index.ts
New useController hook added and exported; returns canAnimate, targetRef, animatedChildren, and text; integrates intersection observation, node splitting, motion resolution, and animated-children orchestration; tests updated for new names/shape.
TextMotion Component & Tests
src/components/TextMotion/TextMotion.tsx, src/components/TextMotion/TextMotion.spec.tsx
Component switched to use useController; shouldAnimatecanAnimate; updated callback ref handling, class names (text-motion-inactive), render paths, and test expectations.
Animated Children (new)
src/hooks/useAnimateChildren/useAnimateChildren.tsx, src/hooks/useAnimateChildren/useAnimateChildren.spec.tsx, src/hooks/useAnimateChildren/index.ts, src/hooks/useAnimateChildren/AnimatedSpan.tsx, src/hooks/useAnimateChildren/AnimatedSpan.spec.tsx
New useAnimateChildren hook and re-exports added; implements per-node animation sequencing, AnimatedSpan behavior adjusted (newline-to-
handling removed/commented); comprehensive tests added.
Deprecated Animated Children (removed)
src/hooks/useAnimatedChildren/useAnimatedChildren.tsx, src/hooks/useAnimatedChildren/useAnimatedChildren.spec.tsx, src/hooks/useAnimatedChildren/index.ts
Old useAnimatedChildren implementation, tests, and barrel re-exports removed (superseded by useAnimateChildren).
Resolve Motion Rename & Exports
src/hooks/useResolveMotion/useResolveMotion.ts, src/hooks/useResolveMotion/useResolveMotion.spec.ts, src/hooks/useResolveMotion/index.ts, src/hooks/useResolvedMotion/index.ts
Renamed useResolvedMotionuseResolveMotion; added barrel exports (motionMap, useResolveMotion); removed old re-exports from previous barrel.
TextMotion Animation Hook Removed
src/hooks/useTextMotionAnimation/useTextMotionAnimation.ts, src/hooks/useTextMotionAnimation/index.ts
Legacy useTextMotionAnimation hook file and its index re-export removed.
Intersection Observer API Change
src/hooks/useIntersectionObserver/useIntersectionObserver.ts, src/hooks/useIntersectionObserver/useIntersectionObserver.spec.tsx
Hook now uses a callback ref (RefCallback<T>) and internal element state; deduplicates observer updates and adjusts unobserve behavior; tests updated.
Validation Refactor
src/hooks/useValidation/useValidation.ts, src/hooks/useValidation/useValidation.spec.tsx, src/hooks/useValidation/validateProps.ts, src/hooks/useValidation/validateProps.spec.tsx, src/hooks/useValidation/validation.ts, src/hooks/useValidation/validation.spec.tsx
Validation logic moved to new validateProps with ValidationResult type; useValidation API simplified to accept props only; new tests added, old validation module and tests removed.
Index / Barrel Adjustments
src/hooks/useController/index.ts, src/hooks/useResolveMotion/index.ts, src/hooks/useAnimateChildren/index.ts, removals in src/hooks/useTextMotionAnimation/index.ts, src/hooks/useResolvedMotion/index.ts
Multiple barrel exports updated/added/removed to reflect new hook names and moved exports.
Styles & Utilities
src/styles/motion.scss, src/utils/splitReactNode/splitReactNode.ts
Narrowed selector from .text-motion span, .node-motion span.text-motion span; consolidated visibility class to .text-motion-inactive; adjusted typeGuards import path to barrel.
Package
package.json
Version bump 0.0.10 → 0.0.11.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Component as TextMotion (Component)
participant Controller as useController
participant IO as useIntersectionObserver
participant Resolver as useResolveMotion
participant Animator as useAnimateChildren
Component->>Controller: initialize with props
Controller->>IO: attach callback ref (targetRef) / start observing
IO-->>Controller: isIntersecting change
Controller->>Controller: compute canAnimate (trigger || isIntersecting)
Controller->>Resolver: resolve motion/preset
Controller->>Animator: request animatedChildren (nodes, motion, order, delays, onAnimationEnd)
Animator-->>Controller: animatedChildren
Controller-->>Component: return { canAnimate, targetRef, animatedChildren, text }
Component->>Component: render -> if canAnimate show animatedChildren else render inactive text

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • kjyong702
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive Title is vague and generic, using the term 'improve' without specifying which hooks or what improvements are made. Replace with a more specific title that indicates the key refactoring (e.g., 'refactor: extract useController and rename animation hooks') or the main goal of the changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed Description addresses the template requirements with key changes highlighted and the refactor checkbox marked, but lacks specifics about which hooks were changed and their new responsibilities.
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.


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

@codecov
Copy link
Copy Markdown

codecov bot commented Jan 18, 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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useResolveMotion/useResolveMotion.ts (1)

14-18: Doc mismatch: motion is not deep-copied.

The hook returns motion as-is, so the JSDoc claim about deep-copying is inaccurate and can mislead callers about mutability expectations.

📝 Update docs (or implement copy)
- * If a `motion` object is provided, it will be deep-copied.
+ * If a `motion` object is provided, it will be returned as-is.
🤖 Fix all issues with AI agents
In `@src/hooks/useIntersectionObserver/useIntersectionObserver.ts`:
- Around line 29-34: When the ref callback (ref) receives a new node, reset the
intersection state so a previous true value doesn't carry over; modify the ref
defined with useCallback to call setElement(node) and also call
setIsIntersecting(false) (or explicitly set to a safe default) whenever the node
changes (including when node is null) so the observed element's visibility state
is cleared immediately before the observer attaches to the new node; update
references to element, setElement, isIntersecting, and setIsIntersecting
accordingly.
🧹 Nitpick comments (10)
src/hooks/useAnimateChildren/AnimatedSpan.tsx (1)

24-26: Remove commented-out newline branch to keep the component readable.

Leaving dead code here makes the intended behavior unclear and adds noise for future maintainers. Consider deleting it or replacing it with a short comment that explicitly documents the new newline behavior.

src/hooks/useAnimateChildren/AnimatedSpan.spec.tsx (1)

34-38: Clean up the commented-out test to avoid ambiguity.

Consider deleting this block or converting it to it.skip(...) with a clear reason so future readers understand the intentional behavior shift.

♻️ Possible cleanup
-  // it('renders <br> when text is newline character', () => {
-  //   const { container } = renderSpan('\n');
-
-  //   expect(container.querySelector('br')).toBeInTheDocument();
-  // });
+  // it.skip('renders <br> when text is newline character', () => {
+  //   // Intentionally disabled: newline is now rendered as text within <span>.
+  //   const { container } = renderSpan('\n');
+  //   expect(container.querySelector('br')).toBeInTheDocument();
+  // });
src/hooks/useAnimateChildren/useAnimateChildren.tsx (1)

88-106: Consider extracting the mutable counter logic for clarity.

The sequenceIndex++ side effect inside map works but mixes iteration with mutation, which can reduce predictability. This is a minor readability concern since the logic is contained within the function scope.

If you want to improve explicitness, consider a reduce or explicit loop, but this is acceptable for the current complexity level.

src/hooks/useAnimateChildren/useAnimateChildren.spec.tsx (2)

105-110: Remove or address commented-out test code.

Commented-out tests create noise and ambiguity about intended behavior. Based on the AI summary, the newline-to-br conversion was removed from AnimatedSpan. Either:

  1. Delete this commented test if the feature is permanently removed.
  2. Add a TODO comment explaining why it's disabled and when it should be revisited.

128-150: Test doesn't fully verify the callback update behavior.

This test only asserts that the second callback hasn't been called after rerender, but doesn't verify that triggering the animation end event would actually invoke the updated callback. Consider completing the test:

♻️ Suggested test improvement
   it('updates onAnimationEnd callback when it changes', () => {
     const firstCallback = jest.fn();
     const secondCallback = jest.fn();
     const { nodes } = splitReactNode('A', split);
 
     const { rerender } = renderHook(
       ({ onAnimationEnd }) =>
         useAnimateChildren({
           nodes,
           initialDelay: 0,
           animationOrder: 'first-to-last',
           motion,
           onAnimationEnd,
         }),
       {
         initialProps: { onAnimationEnd: firstCallback },
       }
     );
 
     rerender({ onAnimationEnd: secondCallback });
 
-    expect(secondCallback).not.toHaveBeenCalled();
+    // Render the animated nodes and trigger animation end
+    const { spans } = renderAnimatedHook(nodes, { onAnimationEnd: secondCallback });
+    fireEvent.animationEnd(spans[0]);
+    
+    expect(firstCallback).not.toHaveBeenCalled();
+    expect(secondCallback).toHaveBeenCalledTimes(1);
   });
src/hooks/useValidation/validateProps.spec.tsx (1)

31-37: Consider using it.each for parameterized tests.

Using it.each instead of .forEach inside a single test provides better test output - each split value would appear as a separate test case in the report, making failures easier to identify.

♻️ Suggested refactor
-  it('accepts all valid split values', () => {
-    (['character', 'word'] as const).forEach(split => {
-      const result = validateProps({ children: 'text', split });
-
-      expect(result.errors).toHaveLength(0);
-    });
-  });
+  it.each(['character', 'word'] as const)('accepts valid split value: %s', split => {
+    const result = validateProps({ children: 'text', split });
+
+    expect(result.errors).toHaveLength(0);
+  });
src/components/TextMotion/TextMotion.spec.tsx (2)

60-74: Remove duplicate test case.

Lines 60-74 and 87-101 test identical behavior: rendering spans when canAnimate is true. The only difference is the comment in line 60. Consider removing one of these tests to reduce maintenance burden.

Suggested fix

Keep the test at lines 60-74 (which has the clarifying comment about trigger="on-load") and remove the duplicate at lines 87-101:

-  it('renders spans when canAnimate is true', () => {
-    const animatedChildren = Array.from(TEXT).map((ch, i) => (
-      <span key={i} aria-hidden="true">
-        {ch}
-      </span>
-    ));
-
-    render(<MockTextMotion hookReturn={{ canAnimate: true, text: TEXT, animatedChildren }}>{TEXT}</MockTextMotion>);
-
-    const container = screen.getByLabelText(TEXT);
-    const spans = container.querySelectorAll<HTMLSpanElement>('span[aria-hidden="true"]');
-
-    expect(spans.length).toBe(TEXT.length);
-    expect(container).toHaveClass('text-motion');
-  });

Also applies to: 87-101


138-148: Consider consolidating duplicate onAnimationStart tests.

Lines 138-148 and 162-172 test the same scenario: onAnimationStart is called when canAnimate is true. The only difference is the parenthetical comment. Consider merging these into a single, well-named test.

Suggested fix

Remove the duplicate test at lines 162-172:

-  it('calls onAnimationStart when canAnimate is true (e.g., intersecting)', () => {
-    const onAnimationStart = jest.fn();
-
-    render(
-      <MockTextMotion hookReturn={{ canAnimate: true, text: TEXT }} onAnimationStart={onAnimationStart}>
-        {TEXT}
-      </MockTextMotion>
-    );
-
-    expect(onAnimationStart).toHaveBeenCalledTimes(1);
-  });

Also applies to: 162-172

src/hooks/useValidation/validateProps.ts (1)

27-45: Inconsistent undefined checks may confuse future maintainers.

Lines 27, 31, 43 use truthy checks (if (props.split)) while lines 35, 39 use explicit undefined checks (if (props.X !== undefined)). While this works correctly (strings need truthy, booleans/numbers need explicit checks), the inconsistency reduces readability.

Consider using explicit undefined checks consistently, or add a brief comment explaining why truthy checks are sufficient for string props.

Suggested fix for consistency
-  if (props.split && !ALLOWED_SPLITS.includes(props.split)) {
+  if (props.split !== undefined && !ALLOWED_SPLITS.includes(props.split)) {
     errors.push(`split must be one of: ${ALLOWED_SPLITS.join(', ')}`);
   }

-  if (props.trigger && !ALLOWED_TRIGGERS.includes(props.trigger)) {
+  if (props.trigger !== undefined && !ALLOWED_TRIGGERS.includes(props.trigger)) {
     errors.push(`trigger must be one of: ${ALLOWED_TRIGGERS.join(', ')}`);
   }
src/hooks/useController/useController.spec.tsx (1)

18-19: Variable names don't match the refactored hook names.

The mock variables use past-tense naming (mockUseResolvedMotion, mockUseAnimatedChildren) while the hooks use imperative naming (useResolveMotion, useAnimateChildren). This reduces predictability and could cause confusion.

Suggested fix
-  const mockUseResolvedMotion = useResolveMotion as jest.Mock;
-  const mockUseAnimatedChildren = useAnimateChildren as jest.Mock;
+  const mockUseResolveMotion = useResolveMotion as jest.Mock;
+  const mockUseAnimateChildren = useAnimateChildren as jest.Mock;

Then update all usages in the file (lines 28, 62, 69, 74, 76, 86, 90, 100).

Repository owner deleted a comment from coderabbitai bot Jan 18, 2026
@shubug1015 shubug1015 marked this pull request as ready for review January 18, 2026 08:05
@shubug1015 shubug1015 merged commit 6f09853 into main Jan 18, 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