Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ The copied context includes the selected element and its component stack with so

## Manual Installation

### shadcn Registry

Install the React Grab component from this GitHub registry:

```bash
npx shadcn@latest add aidenybai/react-grab/react-grab
```

Render it once near the root of your app:

```tsx
import { ReactGrab } from "@/components/react-grab";

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
{process.env.NODE_ENV === "development" && <ReactGrab />}
</>
);
}
```

If you cannot use the CLI, install React Grab manually for your framework:

#### Next.js (App router)
Expand Down
23 changes: 23 additions & 0 deletions packages/grab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ The copied context includes the selected element and its component stack with so

## Manual Installation

### shadcn Registry

Install the React Grab component from this GitHub registry:

```bash
npx shadcn@latest add aidenybai/react-grab/react-grab
```

Render it once near the root of your app:

```tsx
import { ReactGrab } from "@/components/react-grab";

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
{process.env.NODE_ENV === "development" && <ReactGrab />}
</>
);
}
```

If you cannot use the CLI, install React Grab manually for your framework:

#### Next.js (App router)
Expand Down
23 changes: 23 additions & 0 deletions packages/react-grab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,29 @@ The copied context includes the selected element and its component stack with so

## Manual Installation

### shadcn Registry
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The new ### shadcn Registry heading accidentally nests the existing manual framework sections under the registry subsection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/react-grab/README.md, line 36:

<comment>The new `### shadcn Registry` heading accidentally nests the existing manual framework sections under the registry subsection.</comment>

<file context>
@@ -33,6 +33,29 @@ The copied context includes the selected element and its component stack with so
 
 ## Manual Installation
 
+### shadcn Registry
+
+Install the React Grab component from this GitHub registry:
</file context>
Suggested change
### shadcn Registry
#### shadcn Registry


Install the React Grab component from this GitHub registry:

```bash
npx shadcn@latest add aidenybai/react-grab/react-grab
```

Render it once near the root of your app:

```tsx
import { ReactGrab } from "@/components/react-grab";

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
{process.env.NODE_ENV === "development" && <ReactGrab />}
</>
);
}
```

If you cannot use the CLI, install React Grab manually for your framework:

#### Next.js (App router)
Expand Down
21 changes: 21 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "react-grab",
"homepage": "https://react-grab.com",
"items": [
{
"name": "react-grab",
"type": "registry:component",
"title": "React Grab",
"description": "Installs and loads React Grab from a React component.",
"dependencies": ["react-grab"],
"files": [
{
"path": "registry/react-grab.tsx",
"type": "registry:component",
"target": "@components/react-grab.tsx"
}
]
}
]
}
60 changes: 60 additions & 0 deletions registry/react-grab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useEffect } from "react";
import type { Options } from "react-grab";

declare global {
interface Window {
__REACT_GRAB_DISABLED__?: boolean;
}
}

interface ReactGrabProps {
enabled?: boolean;
options?: Options;
}

const reactGrabOptions: Options = {
// activationMode: "toggle",
// activationKey: "Alt+KeyG",
// keyHoldDuration: 500,
// allowActivationInsideInput: true,
};

export const ReactGrab = (props: ReactGrabProps) => {
useEffect(() => {
if (props.enabled === false) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Enabled prop never deactivates

Medium Severity

Setting enabled={false} only skips starting a new load; it does not disable or dispose an already-initialized React Grab instance. After the prop becomes false, the overlay and listeners keep running.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 094ba5b. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When enabled becomes false, disable the existing global API instead of returning immediately; otherwise an already initialized instance keeps running.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At registry/react-grab.tsx, line 29:

<comment>When `enabled` becomes false, disable the existing global API instead of returning immediately; otherwise an already initialized instance keeps running.</comment>

<file context>
@@ -0,0 +1,63 @@
+
+export const ReactGrab = (props: ReactGrabProps) => {
+  useEffect(() => {
+    if (props.enabled === false) return;
+
+    let isActive = true;
</file context>


let isActive = true;

const loadReactGrab = async () => {
window.__REACT_GRAB_DISABLED__ = true;
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The import("react-grab") call is inside a try block but there is no catch clause. If the dynamic import fails (e.g., package not installed, network error in dev), the error is silently swallowed with no console warning or user feedback, making debugging difficult.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At registry/react-grab.tsx, line 35:

<comment>The `import("react-grab")` call is inside a `try` block but there is no `catch` clause. If the dynamic import fails (e.g., package not installed, network error in dev), the error is silently swallowed with no console warning or user feedback, making debugging difficult.</comment>

<file context>
@@ -0,0 +1,63 @@
+
+    const loadReactGrab = async () => {
+      window.__REACT_GRAB_DISABLED__ = true;
+      try {
+        const reactGrab = await import("react-grab");
+
</file context>

const reactGrab = await import("react-grab");

if (!isActive) return;

const existingApi = reactGrab.getGlobalApi();
if (existingApi) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Updated options prop ignored

Medium Severity

The effect depends on props.options, but when getGlobalApi() is already set it returns early and never merges new options. Re-renders with different options therefore leave the original configuration in place.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 094ba5b. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Existing global instance short-circuits the effect, so enabled/options prop changes are ignored after first init.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At registry/react-grab.tsx, line 41:

<comment>Existing global instance short-circuits the effect, so `enabled`/`options` prop changes are ignored after first init.</comment>

<file context>
@@ -0,0 +1,63 @@
+        if (!isActive) return;
+
+        const existingApi = reactGrab.getGlobalApi();
+        if (existingApi) return;
+
+        const api = reactGrab.init({
</file context>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Disposed API blocks reinitialization

Medium Severity

After dispose() on the global API, window.__REACT_GRAB__ can still hold the old instance while hasInited is reset, so ReactGrab treats getGlobalApi() as already initialized and never runs init again.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3634587. Configure here.


const api = reactGrab.init({
...reactGrabOptions,
...props.options,
});
Comment on lines +43 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Guard against options.enabled === false before calling init(). Initializing with disabled options can produce a no-op API and block later creation of a real instance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At registry/react-grab.tsx, line 43:

<comment>Guard against `options.enabled === false` before calling `init()`. Initializing with disabled options can produce a no-op API and block later creation of a real instance.</comment>

<file context>
@@ -0,0 +1,63 @@
+        const existingApi = reactGrab.getGlobalApi();
+        if (existingApi) return;
+
+        const api = reactGrab.init({
+          ...reactGrabOptions,
+          ...props.options,
</file context>
Suggested change
const api = reactGrab.init({
...reactGrabOptions,
...props.options,
});
if (props.options?.enabled === false) return;
const api = reactGrab.init({
...reactGrabOptions,
...props.options,
});


reactGrab.setGlobalApi(api);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Concurrent init overwrites global API

High Severity

Overlapping loadReactGrab runs can both pass the getGlobalApi() check before either calls setGlobalApi. The second init() then returns a noop API because hasInited is already true, and setGlobalApi may replace the working global instance with that noop, leaving React Grab inactive.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 094ba5b. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pending plugins never flushed

Medium Severity

After manual init() and setGlobalApi(), the component never flushes plugins that were queued via registerPlugin() while the global API was still null. The default package import path runs flushPendingPlugins on auto-init, but this lazy-init path skips that step.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 094ba5b. Configure here.

window.dispatchEvent(new CustomEvent("react-grab:init", { detail: api }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Noop init published globally

High Severity

When init() returns a noop API (for example options.enabled === false), the component still calls setGlobalApi and dispatches react-grab:init. That noop is truthy, so later runs bail on getGlobalApi() and never install a real instance even if options change.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 094ba5b. Configure here.

} finally {
delete window.__REACT_GRAB_DISABLED__;
}
};

void loadReactGrab();

return () => {
isActive = false;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unmount leaves grab running

Medium Severity

The effect cleanup only clears a local isActive flag and never calls dispose on the initialized global API, so removing ReactGrab from the tree leaves listeners, overlay, and other grab resources active.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3634587. Configure here.

}, [props.enabled, props.options]);
Comment thread
vercel[bot] marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: props.options is an object included in the useEffect dependency array. If the parent passes an inline object literal (e.g., options={{ activationMode: 'toggle' }}), the reference changes every render, causing the effect to fire on every render — re-importing the module, checking the global API, and running through the try/finally block each time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At registry/react-grab.tsx, line 60:

<comment>`props.options` is an object included in the `useEffect` dependency array. If the parent passes an inline object literal (e.g., `options={{ activationMode: 'toggle' }}`), the reference changes every render, causing the effect to fire on every render — re-importing the module, checking the global API, and running through the try/finally block each time.</comment>

<file context>
@@ -0,0 +1,63 @@
+    return () => {
+      isActive = false;
+    };
+  }, [props.enabled, props.options]);
+
+  return null;
</file context>


return null;
};
Loading