Skip to content

Commit 4ef439e

Browse files
committed
fix: isolate DocSearch render errors
Wrap the DocSearch portal in a search-specific error boundary so result rendering crashes show a local search fallback instead of tripping the app route error boundary. Add a regression test that verifies surrounding page content stays rendered.
1 parent 3e0ac90 commit 4ef439e

2 files changed

Lines changed: 74 additions & 16 deletions

File tree

__tests__/Search_.test.res

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ let makeHit = (~type_: DocSearch.contentType, ~url: string): DocSearch.docSearch
2727
_snippetResult: {content: Nullable.null},
2828
}
2929

30+
module ThrowsOnRender = {
31+
@react.component
32+
let make = (): React.element => failwith("search render exploded")
33+
}
34+
3035
// ---------------------------------------------------------------------------
3136
// markdownToHtml
3237
// ---------------------------------------------------------------------------
@@ -250,6 +255,22 @@ test(
250255
},
251256
)
252257

258+
test("search error boundary catches render errors without replacing surrounding page", async () => {
259+
await viewport(1440, 500)
260+
261+
let screen = await render(
262+
<div>
263+
<span> {React.string("Docs page stays rendered")} </span>
264+
<Search.ErrorBoundary onClose={() => ()}>
265+
<ThrowsOnRender />
266+
</Search.ErrorBoundary>
267+
</div>,
268+
)
269+
270+
await element(await screen->getByText("Docs page stays rendered"))->toBeVisible
271+
await element(await screen->getByText("Search unavailable"))->toBeVisible
272+
})
273+
253274
// ---------------------------------------------------------------------------
254275
// isChildHit
255276
// ---------------------------------------------------------------------------

src/components/Search.res

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,46 @@ let hitComponent = ({hit, children: _}: DocSearch.hitComponent): React.element =
227227
</a>
228228
}
229229

230+
module ErrorBoundary = {
231+
@react.component
232+
let make = (~children: React.element, ~onClose: unit => unit) => {
233+
<RescriptReactErrorBoundary
234+
fallback={_ =>
235+
<div
236+
role="alert"
237+
className="fixed top-6 left-1/2 z-1000 flex w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 items-center justify-between gap-4 rounded-sm border border-gray-20 bg-white px-4 py-3 shadow-lg"
238+
>
239+
<span className="text-14 font-medium text-gray-80">
240+
{React.string(unavailableText)}
241+
</span>
242+
<button
243+
type_="button"
244+
ariaLabel="Close search"
245+
className="text-gray-60 hover:text-fire-50 cursor-pointer"
246+
onClick={_ => onClose()}
247+
>
248+
<Icon.Close className="h-4 w-4 stroke-current" />
249+
</button>
250+
</div>}
251+
>
252+
children
253+
</RescriptReactErrorBoundary>
254+
}
255+
}
256+
230257
@react.component
231258
let make = () => {
232259
let (state, setState) = React.useState(_ => Inactive)
233260
let algoliaConfig = Env.algoliaPublicConfig
234261

262+
let deactivateSearch = () => {
263+
switch WebAPI.Document.querySelector(document, "body") {
264+
| Value(body) => WebAPI.DOMTokenList.remove(body.classList, "DocSearch--active")
265+
| Null => ()
266+
}
267+
setState(_ => Inactive)
268+
}
269+
235270
let handleCloseModal = () => {
236271
let () = switch WebAPI.Document.querySelector(document, ".DocSearch-Modal") {
237272
| Value(modal) =>
@@ -243,7 +278,7 @@ let make = () => {
243278
})
244279
| Null => setState(_ => Inactive)
245280
}
246-
| Null => ()
281+
| Null => deactivateSearch()
247282
}
248283
}
249284

@@ -314,21 +349,23 @@ let make = () => {
314349
switch ReactDOM.querySelector("body") {
315350
| Some(element) =>
316351
ReactDOM.createPortal(
317-
<DocSearch
318-
apiKey=searchApiKey
319-
appId
320-
indexName
321-
navigator={navigator(~siteUrl=Env.root_url)}
322-
transformItems={items => normalizeHitUrls(items, ~siteUrl=Env.root_url)}
323-
hitComponent
324-
onClose
325-
initialScrollY={window.scrollY->Float.toInt}
326-
searchParameters={
327-
distinct: 3,
328-
hitsPerPage: 20,
329-
attributesToSnippet: ["content:9999"],
330-
}
331-
/>,
352+
<ErrorBoundary onClose=deactivateSearch>
353+
<DocSearch
354+
apiKey=searchApiKey
355+
appId
356+
indexName
357+
navigator={navigator(~siteUrl=Env.root_url)}
358+
transformItems={items => normalizeHitUrls(items, ~siteUrl=Env.root_url)}
359+
hitComponent
360+
onClose
361+
initialScrollY={window.scrollY->Float.toInt}
362+
searchParameters={
363+
distinct: 3,
364+
hitsPerPage: 20,
365+
attributesToSnippet: ["content:9999"],
366+
}
367+
/>
368+
</ErrorBoundary>,
332369
element,
333370
)
334371
| None => React.null

0 commit comments

Comments
 (0)