Skip to content

Commit 1c5814b

Browse files
justin808claude
andcommitted
docs(client): refine renderer-function teardown examples from PR review
Apply review feedback to the renderer-function teardown docs (PR #3585): - view-helpers-api.md: rewrite the React 18 teardown example as an explicit if/else (no double innerHTML read) and add an `if (!domNode) throw` guard so a missing element yields a named-id error instead of a cryptic null deref; add the same guard to the React 16/17 example. - view-helpers-api.md: note that a dropped async teardown is logged via console.error (diagnosable, not silent), matching the core ClientRenderer. - render-functions.md: use react-dom/client's hydrateRoot (with import) to match the view-helpers example instead of the invalid ReactDOM.hydrateRoot, and clarify the LazyHydrate teardown is only registered after hydration runs. Docs-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bae8c50 commit 1c5814b

2 files changed

Lines changed: 20 additions & 7 deletions

File tree

docs/oss/api-reference/view-helpers-api.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,15 @@ import ReactDOMClient from 'react-dom/client';
118118
// Renderer function: 3 params, mounts itself, returns a teardown.
119119
const MyRenderer = (props, _railsContext, domNodeId) => {
120120
const domNode = document.getElementById(domNodeId);
121-
const root = domNode.innerHTML
122-
? ReactDOMClient.hydrateRoot(domNode, <MyComponent {...props} />)
123-
: ReactDOMClient.createRoot(domNode);
124-
if (!domNode.innerHTML) {
121+
if (!domNode) {
122+
throw new Error(`Missing DOM element with id: ${domNodeId}`);
123+
}
124+
125+
let root;
126+
if (domNode.innerHTML) {
127+
root = ReactDOMClient.hydrateRoot(domNode, <MyComponent {...props} />);
128+
} else {
129+
root = ReactDOMClient.createRoot(domNode);
125130
root.render(<MyComponent {...props} />);
126131
}
127132

@@ -137,13 +142,17 @@ import ReactDOM from 'react-dom';
137142

138143
const MyLegacyRenderer = (props, _railsContext, domNodeId) => {
139144
const domNode = document.getElementById(domNodeId);
145+
if (!domNode) {
146+
throw new Error(`Missing DOM element with id: ${domNodeId}`);
147+
}
148+
140149
ReactDOM.render(<MyComponent {...props} />, domNode);
141150
return () => ReactDOM.unmountComponentAtNode(domNode);
142151
};
143152
```
144153

145154
> [!NOTE]
146-
> Synchronous teardowns are always honored. An **async** teardown is best-effort in the open-source package: if a navigation or node replacement happens before the renderer resolves its teardown, that still-pending teardown may be dropped. React on Rails Pro's client renderer awaits the renderer and handles this race reliably.
155+
> Synchronous teardowns are always honored. An **async** teardown is best-effort in the open-source package: if a navigation or node replacement happens before the renderer resolves its teardown, that still-pending teardown may be dropped (React on Rails logs a `console.error` when this happens, so the dropped teardown is diagnosable rather than silent). React on Rails Pro's client renderer awaits the renderer and handles this race reliably.
147156
148157
---
149158

docs/oss/core-concepts/render-functions.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ A few important points about the detection:
2020
- **`fn.renderFunction = true` is an escape hatch** for render functions that don't need `railsContext` but still want to be treated as render functions (e.g., so they can return a hash). Without the flag, a one-parameter function is classified as a regular React component.
2121

2222
```jsx
23+
import ReactDOMClient from 'react-dom/client';
24+
2325
// Regular React Component — 0 or 1 params, renders normally
2426
const HelloMessage = (props) => <div>Hello {props.name}</div>;
2527

@@ -42,10 +44,12 @@ HelloHash.renderFunction = true;
4244
// Optionally return a teardown (or a promise resolving to one); React on Rails runs it on
4345
// Turbo/Turbolinks navigation (or same-id node replacement) so the root is unmounted, not leaked.
4446
const LazyHydrate = (props, railsContext, domNodeId) =>
45-
// whenVisible is a hypothetical helper that resolves when the element scrolls into view
47+
// whenVisible is a hypothetical helper that resolves when the element scrolls into view.
48+
// The teardown is registered only after hydration runs, so navigating away before the element
49+
// becomes visible leaves nothing mounted to clean up.
4650
whenVisible(domNodeId).then(() => {
4751
const domNode = document.getElementById(domNodeId);
48-
const root = ReactDOM.hydrateRoot(domNode, <HelloMessage {...props} />);
52+
const root = ReactDOMClient.hydrateRoot(domNode, <HelloMessage {...props} />);
4953
return () => root.unmount();
5054
});
5155

0 commit comments

Comments
 (0)