Skip to content

Fix CustomCode scriptsClientOnly not re-running scripts after hydration - #4848

Open
Manish-Builder-io wants to merge 3 commits into
mainfrom
ai_main_11d532f23f2448839fcf
Open

Manish-Builder-io wants to merge 3 commits into
mainfrom
ai_main_11d532f23f2448839fcf

Conversation

@Manish-Builder-io

@Manish-Builder-io Manish-Builder-io commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a regression where CustomCode blocks with scriptsClientOnly: true fail to restore SSR-stripped <script> tags after hydration.

Root cause: shouldComponentUpdate in CustomCodeComponent only compared nextProps.code !== this.props.code. This meant the setState({ hydrated: true }) call made in componentDidMount (which is meant to trigger a one-time re-render to re-insert the scripts that were stripped for SSR) was blocked, since only state.hydrated changed and not props.code.

Fix:

  • Added a proper State type ({ hydrated: boolean }) and used it as the generic parameter on React.Component<Props, State> instead of leaving the component untyped for state.
  • Updated shouldComponentUpdate to also accept nextState: State and return true when nextState.hydrated !== this.state.hydrated, in addition to the existing code prop comparison. This preserves the original perf optimization (skipping re-renders when code is unchanged) while no longer suppressing the one-time hydration re-render.

Tests:

  • Added packages/react/test/custom-code.test.tsx covering:
    • Scripts are correctly stripped from SSR output when scriptsClientOnly is true.
    • Scripts are restored in the DOM and executed after hydration (verifying the fix).
    • CustomCode still skips unnecessary re-renders when neither code nor hydration state change (verifying no perf regression).
  • Updated packages/react/test/setupTests.ts to polyfill TextEncoder/TextDecoder in the jsdom test environment, which react-dom/server's renderToString requires but jsdom 19 doesn't provide.
  • Added a changeset documenting the patch fix for @builder.io/react.

Link to JIRA ticket:
https://builder-io.atlassian.net/browse/SUPP-2847


Edit in Builder  Preview


To clone this PR locally use the Github CLI with command gh pr checkout 4848

You can tag me at @BuilderIO for anything you want me to fix or change


Note

Low Risk
Narrow lifecycle fix in CustomCode with regression tests; behavior change is restoring intended client-only script execution after hydration.

Overview
Fixes a regression where Custom Code blocks with scriptsClientOnly stripped <script> tags on SSR but never put them back or executed them on the client.

shouldComponentUpdate only compared props.code, so the one-time setState({ hydrated: true }) in componentDidMount did not trigger a re-render. The change types component state as { hydrated: boolean } and allows updates when hydrated changes, while still skipping re-renders when only unrelated parent props change.

Adds jsdom tests for SSR script stripping, post-hydration script restore/execution, and a render-count guard. Test setup polyfills TextEncoder/TextDecoder for renderToString. Patch changeset for @builder.io/react.

Reviewed by Cursor Bugbot for commit 274496e. Bugbot is set up for automated code reviews on this repo. Configure here.

@builder-io-integration builder-io-integration Bot changed the title Update from the Builder.io agent Fix CustomCode scriptsClientOnly not re-running scripts after hydration Sep 7, 2026
@nx-cloud

nx-cloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 274496e

Command Status Duration Result
nx test @snippet/react ✅ Succeeded 1m 4s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-09-08 06:49:38 UTC

builder-io-integration[bot]

This comment was marked as outdated.

@Manish-Builder-io

Copy link
Copy Markdown
Contributor Author

@builderio-bot Review all unresolved PR comments and latest review feedback.

Be skeptical and verify each suggestion against the code and requirements before making changes.

For every comment:

  • Fix it if you agree and reply with what changed.
  • If you disagree, reply with a concise rationale.
  • Resolve the comment once addressed.
  • Leave it open only if human input is required.

Process every comment and finish with a summary of fixed, rejected, and blocked items.

@builder-io-integration

Copy link
Copy Markdown
Contributor

All checks pass. Summary of this round:

Fixed (1)

react-dom/client import in the new test — valid finding, agreed. I verified it before changing anything: packages/react/node_modules/@types/react-dom is 16.9.24 and has no client.d.ts. My typecheck was only passing because resolution fell through to the hoisted root @types/react-dom@18.2.17 — exactly the accident the bot flagged.

  • Replaced createRoot/hydrateRoot with @testing-library/react (14.1.2, already a direct devDependency): render(tree, { container, hydrate: true }) for hydration, rerender(...) for the re-render guard.
  • Dropped the now-unneeded IS_REACT_ACT_ENVIRONMENT global declaration.
  • tsc --traceResolution confirms every react-dom* specifier now resolves to the local 16.9.24 copy. react-dom/server stays (exists in @types/react-dom@16, already used by test/basic.test.tsx).

Replied on the thread and resolved it.

Found while verifying (not raised by any reviewer)

CI runs yarn g:nx test @builder.io/react -- packages/react/src, so nothing under packages/react/test/ ever executes in CI — my regression test was guarding nothing. Moved it to packages/react/src/blocks/custom-code.test.tsx and confirmed with the exact CI command (nx test @builder.io/react -- packages/react/src → 4 suites, 41 tests, pass).

Rejected: none. Blocked: none.

Verification: exact CI command passes; full package suite 48 passed; tsc --module commonjs clean (exit 0); prettier clean. Regression still proven both ways — reverting only the nextState.hydrated clause makes the hydration test fail. The two failing suites (test/basic.test.tsx, test/image.test.tsx) are pre-existing Cannot find module 'src/functions/extract-localized-values' errors from a baseUrl-style import at HEAD, outside CI's scope and unrelated.

One thing I left alone deliberately: the merge from main brought in test/variants-ab-hydration.test.tsx, which has its own local TextEncoder/TextDecoder polyfill now duplicated by the guarded one I added to test/setupTests.ts. Harmless, but I didn't touch a file that just landed — say the word if you'd like it de-duplicated.

@changeset-bot

changeset-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 274496e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@builder.io/react Patch

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

@builder-io-integration builder-io-integration Bot left a comment

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.

Builder reviewed your changes — looks good ✅

Review Details

Incremental Code Review Summary

The latest PR head retains the same focused implementation: CustomCode now types its hydration state and allows the one-time hydrated transition to trigger the render needed to restore client-only scripts, while preserving the code-based render bailout. The latest diff introduces no new production-code or test-isolation regressions. Parallel reviewers also validated the focused hydration tests and package build/test paths; broader setup output is affected by the environment's isolated-vm install failure, and one reviewer noted unrelated pre-existing test/build issues.

The previously reported high-severity compatibility finding remains open: packages/react/test/custom-code.test.tsx still imports React 18-only react-dom/client APIs despite the package supporting React/ReactDOM >=16.8 and declaring React DOM 16 types. That existing comment is intentionally not resubmitted during this incremental review.

Risk remains standard because this changes shared React lifecycle behavior and package test/build compatibility.

🧪 Browser testing: Skipped — dev server unavailable on HEAD because dependency setup fails while building isolated-vm; will retry on the next incremental review.

@Manish-Builder-io
Manish-Builder-io requested review from a team and sanyamkamat and removed request for a team September 7, 2026 12:18

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.

why was this needed?

@@ -0,0 +1,5 @@
---

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.

Will do a dev release first

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@sanyamkamat can we do the dev release?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants