Skip to content

fix(LinkPreview): avoid render loop on persistent errors (release 0.4.3)#11

Merged
azizbecha merged 3 commits into
mainfrom
fix/render-loop
Apr 27, 2026
Merged

fix(LinkPreview): avoid render loop on persistent errors (release 0.4.3)#11
azizbecha merged 3 commits into
mainfrom
fix/render-loop

Conversation

@azizbecha

Copy link
Copy Markdown
Owner

Summary

Fixes a render-loop bug in <LinkPreview /> triggered by callers passing inline onSuccess / onError arrows when the URL produces a persistent error.

Repro

function App() {
  return (
    <LinkPreview
      url="https://example.com/this-will-error"
      onError={(e) => console.log(e)}  // inline, fresh ref each render
    />
  );
}

When the URL errors and the error state persists, React fires Maximum update depth exceeded — the component pegs the CPU and never settles.

Root cause

The component listed callback props in effect dependency arrays:

useEffect(() => { if (data) onSuccess?.(data); }, [data, onSuccess]);
useEffect(() => { if (error) onError?.(error); }, [error, onError]);

An inline arrow gets a new function reference on every render. When the consumer's onError runs and triggers a setState, the consumer re-renders, creating a new onError reference, which re-fires the effect, which calls onError again, etc. For success the loop fires once per render too — wasteful but not infinite. For a persistent error it loops forever.

Fix

Read the latest callback through a ref each invocation. Effects now depend only on data / error, so they fire exactly when the underlying state changes — independent of how the consumer wires up their callback.

const onSuccessRef = useRef(onSuccess);
const onErrorRef = useRef(onError);
useEffect(() => { onSuccessRef.current = onSuccess; });
useEffect(() => { onErrorRef.current = onError; });

useEffect(() => { if (data) onSuccessRef.current?.(data); }, [data]);
useEffect(() => { if (error) onErrorRef.current?.(error); }, [error]);

No public API change. Behavior is unchanged for stable (useCallback'd) callbacks, and now also correct for inline arrows.

Verification

  • ✅ Existing test suite still passes (90/90 across 7 files).
  • pnpm prepare (bob build) clean.
  • ✅ Reproduced the loop locally without the fix; cannot reproduce after.

Release

Version bumped from 0.4.20.4.3. On merge to main, release.yml triggers and publishes react-native-preview-url@0.4.3 to npm with provenance.

Test plan

  • CI green
  • After merge, watch the Release workflow on main and confirm 0.4.3 lands on npm

Note

The same fix is already on feat/docs (commit d4e3a19) — once this PR merges, that branch can be rebased on main and the duplicate commit will deduplicate via patch-id.

LinkPreview's success/error effects listed the callback props in their
dependency arrays:

  useEffect(() => { if (data) onSuccess?.(data); }, [data, onSuccess]);
  useEffect(() => { if (error) onError?.(error); }, [error, onError]);

Any consumer that passed an inline arrow (a fresh function reference
each render) would re-fire the effect every render. For an `error`
that persists, that drives an infinite loop:

  effect runs -> onError fires -> consumer setState -> consumer
  re-renders -> new onError reference -> effect runs again ...

Fix: read the latest callback through a ref each invocation. The
effects now depend only on `data` / `error`, so they fire exactly when
the underlying state changes — independent of how the consumer wires
up their callback. No new public API; behavior unchanged for stable
(useCallback'd) callbacks. Existing test suite (90/90) still green.
`pnpm install` auto-runs workspace packages' `prepare` scripts. The root
`package.json` also had its own `prepare` that delegated back to the same
lib via `pnpm --filter`, which fired in parallel with the auto-triggered
one. Two `bob build` processes raced — one cleaned `lib/module/` while
the other was mid-write — surfacing as ENOENT on .map files in CI.

Rename the root convenience script from `prepare` to `build` so it's no
longer a lifecycle hook. Workspace install runs the lib's prepare once;
CI/release call `pnpm build` explicitly when they want a fresh artifact.
@azizbecha azizbecha merged commit 8aac4f2 into main Apr 27, 2026
4 checks passed
@azizbecha azizbecha deleted the fix/render-loop branch April 27, 2026 06:18
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