Skip to content

feat(react): add loader data invalidation support#681

Draft
orionmiz wants to merge 5 commits intomainfrom
vk/6a7c-loader-data-inva
Draft

feat(react): add loader data invalidation support#681
orionmiz wants to merge 5 commits intomainfrom
vk/6a7c-loader-data-inva

Conversation

@orionmiz
Copy link
Collaborator

@orionmiz orionmiz commented Feb 6, 2026

Summary

  • Add useLoader() hook that returns { data, invalidate } for explicit loader invalidation control
  • Add shouldInvalidate callback option for activity state-based automatic invalidation
  • Maintain full backward compatibility with existing useLoaderData() API

Changes

@stackflow/config

  • Add ActivityLoaderConfig union type (Function | { fn, shouldInvalidate })
  • Add getLoaderFn() and getShouldInvalidate() utility functions
  • Add unit tests for utility functions

@stackflow/react

  • Create ActivityLoaderContext for per-activity loader data management
  • Create ActivityLoaderProvider with Core store subscription for state change detection
  • Implement useLoader() hook with runtime loader validation
  • Update loaderPlugin with wrapActivity hook to wrap activities with provider
  • Update useLoaderData() to prefer context when available (backward compatible)

Usage

// Config with shouldInvalidate
{
  name: "Article",
  loader: {
    fn: articleLoader,
    shouldInvalidate: ({ prevActivity, currentActivity }) => {
      return !prevActivity.isActive && currentActivity.isActive;
    },
  },
}

// Component with explicit invalidation
const { data, invalidate } = useLoader({ loaderFn: articleLoader });

orionmiz and others added 4 commits February 6, 2026 18:44
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…port

- Add ActivityLoaderConfig union type (function | object with fn + shouldInvalidate)
- Add getLoaderFn and getShouldInvalidate utility functions
- Update ActivityDefinition to use ActivityLoaderConfig

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create ActivityLoaderContext for per-activity loader data management
- Create ActivityLoaderProvider with Core store subscription
- Implement useLoader hook with runtime loader validation
- Add wrapActivity to loaderPlugin for provider wrapping
- Update useLoaderData to prefer context (backward compatible)
- Export useLoader from future API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ties

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Feb 6, 2026

⚠️ No Changeset found

Latest commit: b7658c4

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

@orionmiz orionmiz marked this pull request as draft February 6, 2026 09:45
@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough

Walkthrough

Introduces a loader data invalidation system across Stackflow's configuration and React integration. Adds ActivityLoaderConfig union type supporting both function and object-based loader specifications with optional shouldInvalidate callbacks. Implements React context, hooks, and provider components for per-activity loader data management with pluggable invalidation policies.

Changes

Cohort / File(s) Summary
Type System & Utilities
config/src/ActivityLoader.ts, config/src/ActivityDefinition.ts, config/src/ActivityLoader.spec.ts
Introduces ActivityLoaderConfig union type (function or object with fn and optional shouldInvalidate), adds helper utilities getLoaderFn and getShouldInvalidate, updates ActivityDefinition to use the new config type, and adds comprehensive tests for both utilities.
Configuration & Build
config/package.json, .pnp.cjs
Adds Jest testing infrastructure (jest, @swc/jest, @types/jest) and test script to package.json. Updates TypeScript type definitions in .pnp.cjs for improved error handling consistency across ZipFS operations.
React Core Infrastructure
integrations/react/src/__internal__/core/CoreProvider.tsx, integrations/react/src/__internal__/core/useCoreSubscribe.ts, integrations/react/src/__internal__/core/index.ts
Introduces CoreSubscribeContext to expose core store subscriptions and adds useCoreSubscribe hook for accessing subscription functionality from React components.
React Loader Lifecycle
integrations/react/src/future/loader/ActivityLoaderContext.tsx, integrations/react/src/future/loader/ActivityLoaderProvider.tsx
Introduces ActivityLoaderContext to manage per-activity loader data and invalidate state, implements ActivityLoaderProvider component that subscribes to core activity changes and triggers invalidation via shouldInvalidate callback.
React Loader Hooks & Integration
integrations/react/src/future/loader/useLoader.ts, integrations/react/src/future/loader/useLoaderData.ts, integrations/react/src/future/loader/loaderPlugin.tsx, integrations/react/src/future/loader/index.ts, integrations/react/src/future/index.ts
Adds useLoader hook with runtime loader validation, updates useLoaderData for backward compatibility with ActivityLoaderContext fallback, extends loaderPlugin with wrapActivity to apply loader provider per-activity, and uses getLoaderFn/getShouldInvalidate utilities throughout.
Core Integration Updates
integrations/react/src/future/stackflow.tsx
Replaces direct loader access with getLoaderFn utility to conditionally resolve and invoke loader functions based on presence of loader configuration.
Documentation
docs/plans/2026-02-04-loader-invalidation-design.md, docs/plans/2026-02-06-loader-invalidation-impl.md
Comprehensive design document for loader invalidation including unopinionated approach, explicit/automatic invalidation via callbacks, backward compatibility considerations, and phased implementation plan. Implementation guide documenting architecture, context setup, hooks, plugin integration, and migration path.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Component
    participant Hook as useLoader Hook
    participant Context as ActivityLoaderContext
    participant Provider as ActivityLoaderProvider
    participant Core as Core Store
    participant Loader as Loader Function

    User->>Hook: useLoader({ loaderFn })
    Hook->>Context: useContext(ActivityLoaderContext)
    Hook->>Hook: Validate loaderFn against config
    
    Note over Provider: On Activity Change
    Core->>Provider: Subscribe to stack changes
    Core-->>Provider: Activity updated
    Provider->>Provider: Call shouldInvalidate?
    alt shouldInvalidate returns true
        Provider->>Loader: Reload data
        Loader-->>Provider: Return new data
        Provider->>Context: Update loaderData
    end
    
    Hook-->>User: { data, invalidate }
    User->>Hook: Call invalidate()
    Hook->>Provider: invalidate()
    Provider->>Loader: Reload data
    Loader-->>Provider: Return new data
    Provider->>Context: Update loaderData
    Context-->>User: Component re-renders with new data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(react): add loader data invalidation support' clearly and concisely describes the main change: adding loader data invalidation functionality to the React integration.
Description check ✅ Passed The description comprehensively covers the changeset, explaining the new useLoader() hook, shouldInvalidate callback, backward compatibility, and changes across both @stackflow/config and @stackflow/react with concrete usage examples.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vk/6a7c-loader-data-inva

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 6, 2026

  • @stackflow/demo

    yarn add https://pkg.pr.new/@stackflow/config@681.tgz
    
    yarn add https://pkg.pr.new/@stackflow/react@681.tgz
    

commit: b7658c4

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 6, 2026

Deploying stackflow-demo with  Cloudflare Pages  Cloudflare Pages

Latest commit: b7658c4
Status: ✅  Deploy successful!
Preview URL: https://6859a728.stackflow-demo.pages.dev
Branch Preview URL: https://vk-6a7c-loader-data-inva.stackflow-demo.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 6, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
stackflow-docs b7658c4 Commit Preview URL Feb 06 2026, 09:55 AM

- Add LoaderOptions interface with shouldInvalidate
- Add function overloads for loader()
- Return ActivityLoaderConfigObject when options provided
- Add tests for loader function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

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

🤖 Fix all issues with AI agents
In `@docs/plans/2026-02-06-loader-invalidation-impl.md`:
- Around line 15-118: Missing test step: add a unit test file for the new
utilities; create config/src/ActivityLoader.spec.ts that tests getLoaderFn and
getShouldInvalidate (covering undefined, function config, and object config
cases), run tests (cd config && yarn test) to ensure they pass, and include the
spec file in the git add/commit alongside ActivityLoader.ts,
ActivityDefinition.ts, and index.ts so the plan and commit are consistent.
- Around line 733-739: The Article component uses React's top-level use hook
(import { use } from "react") to unwrap the loader promise (const resolved =
use(data)), which requires React 19 but the demo declares React 18.3.1; fix by
either bumping the demo's React dependency to 19+ in package.json or remove the
use hook and switch Article to a React 18-compatible pattern (e.g., keep
useLoader({ loaderFn: articleLoader }) and render using Suspense + an async
resource handling or await the loader result in an effect), updating references
to use, resolved, and useLoader accordingly so the component no longer calls use
from React 19 when running under 18.x.

In `@integrations/react/src/future/loader/ActivityLoaderProvider.tsx`:
- Around line 36-65: The issue is prevActivityRef can be stale after the effect
re-subscribes; before calling subscribe in the useEffect, initialize
prevActivityRef.current to the current activity from actions.getStack() (e.g.,
find by activity.id) or null so the first subscription callback uses an
up-to-date prevActivity; keep the rest of the subscribe callback logic
(shouldInvalidate, loadData, setLoaderData) unchanged and ensure you still
update prevActivityRef.current inside the callback after handling invalidation.
🧹 Nitpick comments (10)
integrations/react/src/future/loader/ActivityLoaderProvider.tsx (2)

67-71: Memoize the context value to avoid unnecessary consumer re-renders.

The { loaderData, invalidate } object literal creates a new reference on every render, causing all context consumers to re-render even when neither field has changed.

♻️ Proposed fix
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+  const contextValue = useMemo(
+    () => ({ loaderData, invalidate }),
+    [loaderData, invalidate],
+  );
+
   return (
-    <ActivityLoaderContext.Provider value={{ loaderData, invalidate }}>
+    <ActivityLoaderContext.Provider value={contextValue}>
       {children}
     </ActivityLoaderContext.Provider>
   );

11-11: activityParams typed as {} is overly permissive.

The empty object type {} in TypeScript matches any non-nullish value, which disables meaningful type checking. Consider using Record<string, string> or aligning with the params type used elsewhere in the codebase.

docs/plans/2026-02-04-loader-invalidation-design.md (1)

29-42: Add a language identifier to the fenced code block.

The ASCII diagram block is missing a language specifier, which triggers a markdownlint warning (MD040). Use text since it's not actual code.

Proposed fix
-```
+```text
 ┌─────────────────────────────────────────────────────┐
integrations/react/src/future/loader/useLoaderData.ts (1)

14-14: Nit: Consider translating the Korean comment to English.

This improves readability for all contributors. Something like: // Prefer loader data from ActivityLoaderProvider context; fall back to activity.context.

integrations/react/src/future/loader/useLoader.ts (1)

25-35: Runtime validation is sound but consider the developer experience for the error path.

The find on line 25 silently returns undefined when the activity isn't in the config, which then causes configLoaderFn to be undefined, and the mismatch error on line 30 fires with a somewhat misleading message — it says "loader mismatch" when the real problem may be that no loader was configured at all for the activity (or the activity name wasn't found in config).

Consider distinguishing these cases for better DX:

Proposed improvement
   const activityConfig = config.activities.find(
     (a) => a.name === activity.name,
   );
   const configLoaderFn = getLoaderFn(activityConfig?.loader);

+  if (!configLoaderFn) {
+    throw new Error(
+      `No loader is configured for "${activity.name}" activity. ` +
+        `Ensure a loader is registered in the config before using useLoader().`,
+    );
+  }
+
   if (options.loaderFn !== configLoaderFn) {
     throw new Error(
       `Loader mismatch: the provided loader does not match the loader ` +
         `registered for "${activity.name}" activity in the config.`,
     );
   }
integrations/react/src/future/loader/loaderPlugin.tsx (1)

81-85: loader variable is unused beyond the guard check — consider renaming for clarity.

loader (the resolved function) is obtained on line 81 but only used as a boolean guard on line 83. It's never invoked — loadData is called instead on line 87. This is functionally correct (you're checking if a loader is configured), but naming it loader suggests it will be used. A small rename would clarify intent.

Optional rename
-          const loader = getLoaderFn(matchActivity?.loader);
+          const hasLoader = !!getLoaderFn(matchActivity?.loader);

-          if (!loader || !matchActivity) {
+          if (!hasLoader || !matchActivity) {
docs/plans/2026-02-06-loader-invalidation-impl.md (4)

181-184: Consider stability of shouldInvalidate prop.

The shouldInvalidate callback is included in the effect's dependency array (line 233), but if this prop is not stable between renders, it will cause the subscription to be recreated unnecessarily. Consider documenting that shouldInvalidate should be extracted as a stable reference from the config, or wrap it with useCallback if it's computed.

💡 Alternative: use a ref for shouldInvalidate
 export function ActivityLoaderProvider({
   activity,
   initialLoaderData,
   loadData,
   shouldInvalidate,
   children,
 }: ActivityLoaderProviderProps) {
   const [loaderData, setLoaderData] = useState(initialLoaderData);
   const actions = useCoreActions();
   const prevActivityRef = useRef<Activity>(activity);
+  const shouldInvalidateRef = useRef(shouldInvalidate);
+  
+  useEffect(() => {
+    shouldInvalidateRef.current = shouldInvalidate;
+  });

   // ... rest of code

   useEffect(() => {
-    if (!shouldInvalidate) {
+    if (!shouldInvalidateRef.current) {
       return;
     }

     const unsubscribe = actions.subscribe(() => {
       // ... 
-      if (shouldInvalidate({ prevActivity, currentActivity })) {
+      if (shouldInvalidateRef.current?.({ prevActivity, currentActivity })) {
         // ...
       }
     });

     return unsubscribe;
-  }, [actions, activity.id, loadData, shouldInvalidate]);
+  }, [actions, activity.id, loadData]);

This prevents unnecessary re-subscriptions when shouldInvalidate reference changes.


300-311: Reference equality check for loader validation may be fragile.

The runtime validation uses strict equality (options.loaderFn !== configLoaderFn) to compare loader functions. This will fail if the loader function reference changes due to wrapping, HOCs, or recreation. While this should work for the typical case where loaders are defined as module-level constants, consider documenting this limitation or using a more robust validation approach.

Potential alternatives:

  1. Document that loaders must be stable references (simplest)
  2. Compare by function.name property (less reliable but more flexible)
  3. Add a symbol or ID to loader functions for identification

For most use cases, the current approach with proper documentation should suffice.


384-386: Unnecessary fragment wrapper.

The fragment <>...</> wrapper is unnecessary when returning activity.render() directly. While harmless, it adds an extra node to the tree.

♻️ Simplify return statement
         if (!matchActivity?.loader) {
-          return <>{activity.render()}</>;
+          return activity.render();
         }

580-596: Reorder mock definitions for clarity.

The useConfig mock (lines 586-594) references mockLoader before it's defined (line 596). While JavaScript hoisting will handle this, it's clearer to define mockLoader first.

♻️ Reorder for better readability
+const mockLoader = vi.fn(() => Promise.resolve({ title: "Test" }));
+
 // Mock dependencies
 vi.mock("../../stable", () => ({
   useActivity: vi.fn(),
 }));

 vi.mock("../config/useConfig", () => ({
   useConfig: vi.fn(() => ({
     activities: [
       {
         name: "TestActivity",
         loader: mockLoader,
       },
     ],
   })),
 }));

-const mockLoader = vi.fn(() => Promise.resolve({ title: "Test" }));

Comment on lines +15 to +118
## Task 1: ActivityLoaderConfig 타입 정의

**Files:**
- Modify: `config/src/ActivityLoader.ts`
- Modify: `config/src/ActivityDefinition.ts`
- Modify: `config/src/index.ts`

**Step 1: ActivityLoaderConfig 타입 추가**

`config/src/ActivityLoader.ts` 파일 끝에 추가:

```typescript
import type { Activity } from "@stackflow/core";

export interface ActivityLoaderConfigObject<
ActivityName extends RegisteredActivityName,
> {
fn: ActivityLoader<ActivityName>;
shouldInvalidate?: (args: {
prevActivity: Activity;
currentActivity: Activity;
}) => boolean;
}

export type ActivityLoaderConfig<ActivityName extends RegisteredActivityName> =
| ActivityLoader<ActivityName>
| ActivityLoaderConfigObject<ActivityName>;
```

**Step 2: 유틸리티 함수 추가**

`config/src/ActivityLoader.ts` 파일에 추가:

```typescript
export function getLoaderFn<ActivityName extends RegisteredActivityName>(
loaderConfig: ActivityLoaderConfig<ActivityName> | undefined,
): ActivityLoader<ActivityName> | undefined {
if (!loaderConfig) {
return undefined;
}
if (typeof loaderConfig === "function") {
return loaderConfig;
}
return loaderConfig.fn;
}

export function getShouldInvalidate<ActivityName extends RegisteredActivityName>(
loaderConfig: ActivityLoaderConfig<ActivityName> | undefined,
): ActivityLoaderConfigObject<ActivityName>["shouldInvalidate"] | undefined {
if (!loaderConfig || typeof loaderConfig === "function") {
return undefined;
}
return loaderConfig.shouldInvalidate;
}
```

**Step 3: ActivityDefinition 타입 수정**

`config/src/ActivityDefinition.ts` 파일의 `loader` 프로퍼티 타입 변경:

```typescript
import type { ActivityLoaderConfig } from "./ActivityLoader";

export interface ActivityDefinition<
ActivityName extends RegisteredActivityName,
> {
name: ActivityName;
loader?: ActivityLoaderConfig<any>; // ActivityLoader에서 ActivityLoaderConfig로 변경
}
```

**Step 4: index.ts에서 export 추가**

`config/src/index.ts`에 추가:

```typescript
export type {
ActivityLoaderConfig,
ActivityLoaderConfigObject,
} from "./ActivityLoader";
export { getLoaderFn, getShouldInvalidate } from "./ActivityLoader";
```

**Step 5: 빌드 확인**

Run: `cd config && yarn build`
Expected: 빌드 성공

**Step 6: Commit**

```bash
git add config/src/ActivityLoader.ts config/src/ActivityDefinition.ts config/src/index.ts
git commit -m "$(cat <<'EOF'
feat(config): add ActivityLoaderConfig type with shouldInvalidate support

- Add ActivityLoaderConfig union type (function | object with fn + shouldInvalidate)
- Add getLoaderFn and getShouldInvalidate utility functions
- Update ActivityDefinition to use ActivityLoaderConfig

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing test file creation step for config utilities.

The AI summary indicates that config/src/ActivityLoader.spec.ts was created with tests for getLoaderFn and getShouldInvalidate, but Task 1 doesn't include a step to create this test file. This creates an inconsistency in the implementation plan.

📝 Suggested addition after Step 5

Add a new step before the final commit:

**Step 5.5: Add tests for utility functions**

Create `config/src/ActivityLoader.spec.ts`:

```typescript
import { describe, expect, it } from "vitest";
import { getLoaderFn, getShouldInvalidate } from "./ActivityLoader";

describe("getLoaderFn", () => {
  it("should return undefined for undefined config", () => {
    expect(getLoaderFn(undefined)).toBeUndefined();
  });

  it("should return the function when config is a function", () => {
    const fn = () => {};
    expect(getLoaderFn(fn)).toBe(fn);
  });

  it("should return fn property when config is an object", () => {
    const fn = () => {};
    expect(getLoaderFn({ fn })).toBe(fn);
  });
});

describe("getShouldInvalidate", () => {
  it("should return undefined for undefined config", () => {
    expect(getShouldInvalidate(undefined)).toBeUndefined();
  });

  it("should return undefined when config is a function", () => {
    expect(getShouldInvalidate(() => {})).toBeUndefined();
  });

  it("should return shouldInvalidate when config is an object", () => {
    const shouldInvalidate = () => true;
    expect(getShouldInvalidate({ fn: () => {}, shouldInvalidate })).toBe(shouldInvalidate);
  });
});

Run: `cd config && yarn test`
Expected: Tests pass
🤖 Prompt for AI Agents
In `@docs/plans/2026-02-06-loader-invalidation-impl.md` around lines 15 - 118,
Missing test step: add a unit test file for the new utilities; create
config/src/ActivityLoader.spec.ts that tests getLoaderFn and getShouldInvalidate
(covering undefined, function config, and object config cases), run tests (cd
config && yarn test) to ensure they pass, and include the spec file in the git
add/commit alongside ActivityLoader.ts, ActivityDefinition.ts, and index.ts so
the plan and commit are consistent.

Comment on lines +733 to +739
import { use } from "react";
import { useLoader } from "@stackflow/react/future";
import { articleLoader } from "./Article.loader";

export const Article = () => {
const { data, invalidate } = useLoader({ loaderFn: articleLoader });
const resolved = use(data);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find demo app package.json files
find . -name "package.json" -path "*/demo*" -type f | head -20

Repository: daangn/stackflow

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Read the React version from demo/package.json
cat ./demo/package.json | jq '.dependencies.react, .devDependencies.react'

Repository: daangn/stackflow

Length of output: 74


React version incompatibility: use hook requires React 19+, but demo app specifies React 18.3.1.

The demo code uses React's use hook to unwrap promises (lines 733, 739), which was introduced in React 19. The demo app's package.json specifies "^18.3.1", which does not support this hook. Either upgrade the React dependency to 19 or use an alternative pattern such as Suspense with async/await.

🤖 Prompt for AI Agents
In `@docs/plans/2026-02-06-loader-invalidation-impl.md` around lines 733 - 739,
The Article component uses React's top-level use hook (import { use } from
"react") to unwrap the loader promise (const resolved = use(data)), which
requires React 19 but the demo declares React 18.3.1; fix by either bumping the
demo's React dependency to 19+ in package.json or remove the use hook and switch
Article to a React 18-compatible pattern (e.g., keep useLoader({ loaderFn:
articleLoader }) and render using Suspense + an async resource handling or await
the loader result in an effect), updating references to use, resolved, and
useLoader accordingly so the component no longer calls use from React 19 when
running under 18.x.

Comment on lines +36 to +65
useEffect(() => {
if (!shouldInvalidate) {
return;
}

const unsubscribe = subscribe(() => {
const stack = actions.getStack();
const currentActivity = stack.activities.find(
(a) => a.id === activity.id,
);

if (!currentActivity) {
return;
}

const prevActivity = prevActivityRef.current;

if (shouldInvalidate({ prevActivity, currentActivity })) {
const newLoaderData = loadData(
currentActivity.name,
currentActivity.params,
);
setLoaderData(newLoaderData);
}

prevActivityRef.current = currentActivity;
});

return unsubscribe;
}, [actions, subscribe, activity.id, loadData, shouldInvalidate]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

prevActivityRef may become stale after effect re-subscription.

When the useEffect deps change (e.g., shouldInvalidate or loadData reference changes), the subscription tears down and re-creates. However, prevActivityRef is not reset — it retains whatever value was last written by the previous subscription. On the first callback of the new subscription, shouldInvalidate may compare against a stale prevActivity from before the teardown, potentially producing an incorrect invalidation decision.

Consider resetting the ref when the effect re-runs:

Proposed fix
   useEffect(() => {
     if (!shouldInvalidate) {
       return;
     }
 
+    prevActivityRef.current = activity;
+
     const unsubscribe = subscribe(() => {
📝 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
useEffect(() => {
if (!shouldInvalidate) {
return;
}
const unsubscribe = subscribe(() => {
const stack = actions.getStack();
const currentActivity = stack.activities.find(
(a) => a.id === activity.id,
);
if (!currentActivity) {
return;
}
const prevActivity = prevActivityRef.current;
if (shouldInvalidate({ prevActivity, currentActivity })) {
const newLoaderData = loadData(
currentActivity.name,
currentActivity.params,
);
setLoaderData(newLoaderData);
}
prevActivityRef.current = currentActivity;
});
return unsubscribe;
}, [actions, subscribe, activity.id, loadData, shouldInvalidate]);
useEffect(() => {
if (!shouldInvalidate) {
return;
}
prevActivityRef.current = activity;
const unsubscribe = subscribe(() => {
const stack = actions.getStack();
const currentActivity = stack.activities.find(
(a) => a.id === activity.id,
);
if (!currentActivity) {
return;
}
const prevActivity = prevActivityRef.current;
if (shouldInvalidate({ prevActivity, currentActivity })) {
const newLoaderData = loadData(
currentActivity.name,
currentActivity.params,
);
setLoaderData(newLoaderData);
}
prevActivityRef.current = currentActivity;
});
return unsubscribe;
}, [actions, subscribe, activity.id, loadData, shouldInvalidate]);
🤖 Prompt for AI Agents
In `@integrations/react/src/future/loader/ActivityLoaderProvider.tsx` around lines
36 - 65, The issue is prevActivityRef can be stale after the effect
re-subscribes; before calling subscribe in the useEffect, initialize
prevActivityRef.current to the current activity from actions.getStack() (e.g.,
find by activity.id) or null so the first subscription callback uses an
up-to-date prevActivity; keep the rest of the subscribe callback logic
(shouldInvalidate, loadData, setLoaderData) unchanged and ensure you still
update prevActivityRef.current inside the callback after handling invalidation.

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