Skip to content

fix(stackflow): scope swipe-back vars to activity roots to cut style recalculation#1204

Open
junghyeonsu wants to merge 1 commit intodevfrom
fix/scope-swipe-back-vars
Open

fix(stackflow): scope swipe-back vars to activity roots to cut style recalculation#1204
junghyeonsu wants to merge 1 commit intodevfrom
fix/scope-swipe-back-vars

Conversation

@junghyeonsu
Copy link
Contributor

@junghyeonsu junghyeonsu commented Feb 3, 2026

전역 컨테이너에 CSS 변수를 매 프레임 갱신하던 구조를, 상단/뒤쪽 풀스크린 액티비티 루트로만 좁히도록 바꾼 게 핵심입니다. 변화점은 다음과 같습니다.

  • 변수 타깃 관리 추가: swipeBackTargetsRef를 두고 resetSwipeBackVars, setSwipeBackVar, applySwipeBackContext로 특정 요소에만 --swipe-back-*를 적용하도록 분리
  • 타깃 선정 로직: updateSwipeBackTargets에서 스택의 top activity와 그 뒤의 첫 full-screen activity만 찾아 CSS 변수를 설정하도록 했고, 이전 타깃에는 변수 제거
  • 전역 스타일 갱신 제거: 기존 stackRef.current?.style.setProperty(...) 호출은 모두 setSwipeBackVar로 교체되어 더 이상 전체 스택 DOM에 변수 상속이 퍼지지 않음
  • 타깃 갱신 시점: useLayoutEffect로 stack.activities 변경에 따라 타깃을 재계산하고 현재 스와이프 컨텍스트를 적용

결과적으로 swipe move 중 스타일 재계산 범위가 “스택 전체” → “상단/뒤쪽 풀스크린 액티비티 루트”로 줄어들어 성능 이슈를 완화하도록 설계했습니다.

Summary by CodeRabbit

릴리스 노트

  • 개선 사항
    • 스와이프백 제스처 처리의 경계 조건 처리가 개선되었습니다.
    • 스와이프 상호작용 이벤트 처리 메커니즘이 최적화되었습니다.

@junghyeonsu junghyeonsu self-assigned this Feb 3, 2026
@changeset-bot
Copy link

changeset-bot bot commented Feb 3, 2026

⚠️ No Changeset found

Latest commit: df1ef83

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

개요

스와이프백 기능의 내부 처리를 개선하기 위해 DOM 래핑, CSS 변수 기반 스타일 관리, 타겟 추적 메커니즘을 도입했습니다. 공개 API는 변경되지 않았으며, 공개 서명이 유지되었습니다.

변경 사항

코호트 / 파일(s) 요약
UI 컴포넌트 앱 스크린
docs/registry/ui/app-screen.tsx, examples/stackflow-spa/src/seed-design/ui/app-screen.tsx
swipeBackBoundaryStyle CSS 상수를 추가하고, 자식 요소를 이를 적용한 div로 래핑하여 스와이프백 변위를 제약합니다. 비 포인터 모드와 PullToRefresh 모드 모두에서 DOM 구조를 변경합니다.
Global Interaction 훅
packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts
swipeBackTargetsRef를 도입하여 여러 타겟 요소를 추적하고, 직접 스타일 조작에서 CSS 커스텀 속성 기반 접근법으로 전환합니다. 레이아웃 효과 기반 동기화와 모듈화된 헬퍼 함수(resetSwipeBackVars, setSwipeBackVar)를 추가합니다.
Swipe Back 훅
packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts
스와이프백 이벤트 생성을 useMemo로 감싸서 메모이제이션하고, 개별 필드를 전달하여 이벤트 객체의 안정성과 재생성 타이밍을 개선합니다.

예상 코드 리뷰 노력

🎯 3 (보통) | ⏱️ ~22분

🐰 스와이프백을 다시 정리하며,
CSS 변수가 춤을 추고,
타겟들이 모여 손을 잡고,
DOM의 경계를 긋는다네.
공개 얼굴은 그대로에,
내심의 로직만 빛나리!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely describes the main change: scoping swipe-back CSS variables to activity roots to reduce style recalculation, which aligns with the primary objective across all modified files.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/scope-swipe-back-vars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cloudflare-workers-and-pages
Copy link

Deploying seed-design-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: df1ef83
Status: ✅  Deploy successful!
Preview URL: https://aa8efb19.seed-design.pages.dev
Branch Preview URL: https://fix-scope-swipe-back-vars.seed-design.pages.dev

View logs

Copy link
Contributor

@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: 2

🤖 Fix all issues with AI agents
In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`:
- Around line 163-175: The velocity computation in moveSwipeBack can divide by
zero when t === swipeBackContextRef.current.t0; modify moveSwipeBack to compute
a safe delta time (e.g., deltaT = t - t0) and if deltaT is 0 or extremely small,
use a fallback (0 or a tiny epsilon) before calculating velocity, then update
setSwipeBackContext with the guarded velocity and keep the rest of the logic
(references: moveSwipeBack, swipeBackContextRef.current.t0, setSwipeBackContext,
setSwipeBackState, onSwipeBackMove).
- Around line 129-131: useLayoutEffect that calls
updateSwipeBackTargets(activities) lacks a cleanup; add a return cleanup
function in the useLayoutEffect inside useGlobalInteraction that resets the CSS
variables (--swipe-back-displacement, --swipe-back-displacement-ratio,
--swipe-back-target) on the elements previously targeted by
updateSwipeBackTargets (use the same activities or stored targets) when the
component unmounts or activities change; implement by capturing the list of
target elements (from activities or from updateSwipeBackTargets return value)
and in the cleanup loop remove or set those CSS custom properties to ''/null so
no stale styles remain.

Comment on lines +129 to +131
useLayoutEffect(() => {
updateSwipeBackTargets(activities);
}, [activities, updateSwipeBackTargets]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

useLayoutEffect에 cleanup 함수가 없습니다.

컴포넌트 언마운트 시 타겟 엘리먼트에 설정된 CSS 변수들(--swipe-back-displacement, --swipe-back-displacement-ratio, --swipe-back-target)이 정리되지 않습니다.

🧹 cleanup 함수 추가 제안
 useLayoutEffect(() => {
   updateSwipeBackTargets(activities);
+  return () => {
+    swipeBackTargetsRef.current.forEach(resetSwipeBackVars);
+    swipeBackTargetsRef.current = [];
+  };
 }, [activities, updateSwipeBackTargets]);
🤖 Prompt for AI Agents
In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`
around lines 129 - 131, useLayoutEffect that calls
updateSwipeBackTargets(activities) lacks a cleanup; add a return cleanup
function in the useLayoutEffect inside useGlobalInteraction that resets the CSS
variables (--swipe-back-displacement, --swipe-back-displacement-ratio,
--swipe-back-target) on the elements previously targeted by
updateSwipeBackTargets (use the same activities or stored targets) when the
component unmounts or activities change; implement by capturing the list of
target elements (from activities or from updateSwipeBackTargets return value)
and in the cleanup loop remove or set those CSS custom properties to ''/null so
no stale styles remain.

Comment on lines +163 to +175
const moveSwipeBack = ({ x, t }: MoveSwipeBackProps) => {
const displacement = x - swipeBackContextRef.current.x0;
const displacementRatio = displacement / window.innerWidth;
const velocity = displacement / (t - swipeBackContextRef.current.t0);
setSwipeBackContext({
...swipeBackContextRef.current,
displacement,
displacementRatio,
velocity,
});
setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
onSwipeBackMove?.({ displacement, displacementRatio });
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

0으로 나누기 가능성이 있습니다.

tt0가 동일한 타임스탬프일 경우 (터치 이벤트가 매우 빠르게 발생할 때) velocity 계산에서 0으로 나누기가 발생하여 Infinity 또는 NaN이 될 수 있습니다.

🛡️ 0으로 나누기 방어 코드 제안
 const moveSwipeBack = ({ x, t }: MoveSwipeBackProps) => {
   const displacement = x - swipeBackContextRef.current.x0;
   const displacementRatio = displacement / window.innerWidth;
-  const velocity = displacement / (t - swipeBackContextRef.current.t0);
+  const timeDelta = t - swipeBackContextRef.current.t0;
+  const velocity = timeDelta > 0 ? displacement / timeDelta : 0;
   setSwipeBackContext({
     ...swipeBackContextRef.current,
     displacement,
     displacementRatio,
     velocity,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const moveSwipeBack = ({ x, t }: MoveSwipeBackProps) => {
const displacement = x - swipeBackContextRef.current.x0;
const displacementRatio = displacement / window.innerWidth;
const velocity = displacement / (t - swipeBackContextRef.current.t0);
setSwipeBackContext({
...swipeBackContextRef.current,
displacement,
displacementRatio,
velocity,
});
setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
onSwipeBackMove?.({ displacement, displacementRatio });
};
const moveSwipeBack = ({ x, t }: MoveSwipeBackProps) => {
const displacement = x - swipeBackContextRef.current.x0;
const displacementRatio = displacement / window.innerWidth;
const timeDelta = t - swipeBackContextRef.current.t0;
const velocity = timeDelta > 0 ? displacement / timeDelta : 0;
setSwipeBackContext({
...swipeBackContextRef.current,
displacement,
displacementRatio,
velocity,
});
setSwipeBackState((prev) => (prev === "swiping" ? prev : "swiping"));
onSwipeBackMove?.({ displacement, displacementRatio });
};
🤖 Prompt for AI Agents
In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`
around lines 163 - 175, The velocity computation in moveSwipeBack can divide by
zero when t === swipeBackContextRef.current.t0; modify moveSwipeBack to compute
a safe delta time (e.g., deltaT = t - t0) and if deltaT is 0 or extremely small,
use a fallback (0 or a tiny epsilon) before calculating velocity, then update
setSwipeBackContext with the guarded velocity and keep the rest of the logic
(references: moveSwipeBack, swipeBackContextRef.current.t0, setSwipeBackContext,
setSwipeBackState, onSwipeBackMove).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant