feat(react): add lifecyclePlugin and useFocusEffect hook#688
feat(react): add lifecyclePlugin and useFocusEffect hook#688
Conversation
Add an internal lifecycle plugin that provides focus/blur callbacks for activities. When an activity becomes active (initial or refocus), registered effects run. When it loses active status, cleanups execute. Detection and invocation happen in the plugin's onChanged hook (outside React render cycle), while the hook only handles registration/deregistration. This avoids useDeferredValue tearing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 9ddcbae The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a React lifecycle plugin (lifecyclePlugin + useFocusEffect) that tracks activity focus/blur and runs per-activity callbacks; integrates the plugin into stack initialization. Also adds Jest tests, updates esbuild entry discovery to exclude spec files, adjusts tsconfig excludes, and updates Yarn PnP virtual mappings in Changes
Sequence DiagramsequenceDiagram
actor ActivityComponent
participant LifecyclePlugin
participant StackflowCore
participant EffectRegistry
ActivityComponent->>LifecyclePlugin: useFocusEffect(callback) — register entry
LifecyclePlugin->>EffectRegistry: store {symbol, activityId, callbackRef}
StackflowCore->>LifecyclePlugin: onChanged(newActiveActivityId)
LifecyclePlugin->>EffectRegistry: detect prevActive -> newActive
LifecyclePlugin->>EffectRegistry: blur: run stored cleanup for prev activity
LifecyclePlugin->>EffectRegistry: focus: invoke callbackRef.current for new activity
EffectRegistry-->>LifecyclePlugin: returns optional cleanup
LifecyclePlugin->>EffectRegistry: store returned cleanup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying stackflow-demo with
|
| Latest commit: |
9ddcbae
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://73eaa740.stackflow-demo.pages.dev |
| Branch Preview URL: | https://edward-karrot-plugin-refocus.stackflow-demo.pages.dev |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
stackflow-docs | 9ddcbae | Commit Preview URL | Mar 30 2026, 10:29 AM |
commit: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
integrations/react/src/future/lifecycle/lifecyclePlugin.spec.tsx (1)
208-251: Consider adding a null guard or explicit assertion forsetUseSecond.The definite assignment assertion (
!:) on line 212 assumessetUseSecondwill be assigned before use. While this works in practice becauserender()synchronously mounts the component, it bypasses TypeScript's null safety.A safer pattern would be to assert assignment or use a ref container:
♻️ Optional: Add explicit assertion for clarity
- let setUseSecond!: (v: boolean) => void; + let setUseSecond: ((v: boolean) => void) | undefined; function ActivityA() { const [useSecond, _setUseSecond] = useState(false); setUseSecond = _setUseSecond; useFocusEffect(useSecond ? secondEffect : firstEffect); return <div>A</div>; } // ... after render ... + expect(setUseSecond).toBeDefined(); + // Update callback while A is active await act(async () => { - setUseSecond(true); + setUseSecond!(true); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@integrations/react/src/future/lifecycle/lifecyclePlugin.spec.tsx` around lines 208 - 251, The test currently uses a definite assignment for setUseSecond in ActivityA which can be undefined; add a null guard or explicit runtime assertion before calling it in the test (or initialize it to a safe stub) so we don't rely on TypeScript's definite assignment. Locate the test's setUseSecond variable and either (a) initialize it to a no-op or throwing stub at declaration, (b) add an assertion like expect(setUseSecond).toBeDefined() before calling setUseSecond(true), or (c) switch to a ref container inside ActivityA and expose a stable setter; update references in this spec where setUseSecond is invoked to use the chosen safe pattern (e.g., the actions inside act blocks).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@integrations/react/src/future/lifecycle/lifecyclePlugin.spec.tsx`:
- Around line 208-251: The test currently uses a definite assignment for
setUseSecond in ActivityA which can be undefined; add a null guard or explicit
runtime assertion before calling it in the test (or initialize it to a safe
stub) so we don't rely on TypeScript's definite assignment. Locate the test's
setUseSecond variable and either (a) initialize it to a no-op or throwing stub
at declaration, (b) add an assertion like expect(setUseSecond).toBeDefined()
before calling setUseSecond(true), or (c) switch to a ref container inside
ActivityA and expose a stable setter; update references in this spec where
setUseSecond is invoked to use the chosen safe pattern (e.g., the actions inside
act blocks).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1f7824fd-2f3e-4145-b6f4-e9427087570a
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (11)
.pnp.cjsintegrations/react/esbuild.config.jsintegrations/react/package.jsonintegrations/react/src/future/index.tsintegrations/react/src/future/lifecycle/index.tsintegrations/react/src/future/lifecycle/lifecyclePlugin.spec.tsxintegrations/react/src/future/lifecycle/lifecyclePlugin.tsxintegrations/react/src/future/lifecycle/runSafely.tsintegrations/react/src/future/lifecycle/useFocusEffect.tsintegrations/react/src/future/stackflow.tsxintegrations/react/tsconfig.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
lifecyclePluginthat detects activity focus/blur transitions viaonChanged(outside React render cycle)useFocusEffect(callback)hook for per-activity lifecycle callbacks, matching React Navigation's API patternuseDeferredValuetearing issuescallbackRefpattern (nouseCallbackrequired by consumers)runSafely()for error isolationUsage
Key design decisions
isActiveoverisTop:isActivechanges immediately on push, whileisTopwaits for exit animationonInitforprevActiveActivityId: Prevents missing the first blur whenonChangedhasn't fired for the initial stateskipInitialomitted: Staleness is delegated to the data layer (e.g. TanStack QuerystaleTime)Test plan
🤖 Generated with Claude Code