///|
/// The JavaScript error passed to a React root error callback.
#external
pub type ReactError

///|
/// Metadata passed to a React root error callback.
#external
pub type ReactErrorInfo

///|
/// Returns the JavaScript Error message reported by React.
pub extern "js" fn ReactError::message(self : ReactError) -> String =
  #| (error) => error?.message ?? String(error)

///|
/// Returns the React component stack associated with a root error.
pub extern "js" fn ReactErrorInfo::component_stack(
  self : ReactErrorInfo,
) -> String =
  #| (info) => info?.componentStack ?? ""

///|
/// Configuration shared by `createRoot` and `hydrateRoot`.
struct RootOptions {
  js_value : @dom.JsObjectObscure
}

///|
/// Creates root options matching React 19's root error and identifier options.
/// `identifier_prefix` must match the server renderer during hydration.
pub fn RootOptions::new(
  identifier_prefix? : String,
  on_caught_error? : (ReactError, ReactErrorInfo) -> Unit,
  on_uncaught_error? : (ReactError, ReactErrorInfo) -> Unit,
  on_recoverable_error? : (ReactError, ReactErrorInfo) -> Unit,
) -> RootOptions {
  let options = @dom.JsObjectObscure::new()
  match identifier_prefix {
    Some(prefix) =>
      options.set("identifierPrefix", @dom.JsObscure::from_string(prefix))
    None => ()
  }
  match on_caught_error {
    Some(callback) =>
      options.set("onCaughtError", @dom.v_to_js_obscure(callback))
    None => ()
  }
  match on_uncaught_error {
    Some(callback) =>
      options.set("onUncaughtError", @dom.v_to_js_obscure(callback))
    None => ()
  }
  match on_recoverable_error {
    Some(callback) =>
      options.set("onRecoverableError", @dom.v_to_js_obscure(callback))
    None => ()
  }
  RootOptions::{ js_value: options }
}

///|
fn RootOptions::to_js_obscure(self : RootOptions) -> @dom.JsObscure {
  self.js_value.to_js_obscure()
}

///|
extern "js" fn react_render(
  vdom : @dom.JsObscure,
  parent : @dom.Node,
  options : @dom.JsObscure,
) -> Unit =
  #| (vdom, parent, options) => {
  #|   const roots = globalThis.__moonbitReactRoots ??= new WeakMap();
  #|   let root = roots.get(parent);
  #|   if (!root) {
  #|     root = globalThis.ReactDOMClient.createRoot(parent, options ?? undefined);
  #|     roots.set(parent, root);
  #|   }
  #|   root.render(vdom);
  #| }

///|
extern "js" fn react_hydrate_root(
  vdom : @dom.JsObscure,
  parent : @dom.Node,
  options : @dom.JsObscure,
) -> Unit =
  #| (vdom, parent, options) => {
  #|   const roots = globalThis.__moonbitReactRoots ??= new WeakMap();
  #|   if (roots.has(parent)) {
  #|     throw new Error("hydrate_root requires a parent without an existing React root");
  #|   }
  #|   const root = globalThis.ReactDOMClient.hydrateRoot(
  #|     parent,
  #|     vdom,
  #|     options ?? undefined,
  #|   );
  #|   roots.set(parent, root);
  #| }

///|
extern "js" fn react_unmount(parent : @dom.Node) -> Unit =
  #| (parent) => {
  #|   const roots = globalThis.__moonbitReactRoots;
  #|   const root = roots?.get(parent);
  #|   if (root) {
  #|     root.unmount();
  #|     roots.delete(parent);
  #|   }
  #| }

///|
extern "js" fn react_create_portal(
  child : @dom.JsObscure,
  parent : @dom.Node,
  key : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (child, parent, key) => key == null
  #|   ? globalThis.ReactDOM.createPortal(child, parent)
  #|   : globalThis.ReactDOM.createPortal(child, parent, key)

///|
/// Cross-origin policies supported by React DOM resource hints.
/// `Anonymous` omits credentials, while `UseCredentials` includes them.
pub(all) enum ResourceCrossOrigin {
  Anonymous
  UseCredentials
} derive(Eq)

///|
fn ResourceCrossOrigin::to_string(self : ResourceCrossOrigin) -> String {
  match self {
    Anonymous => "anonymous"
    UseCredentials => "use-credentials"
  }
}

///|
/// Browser fetch priorities supported by React DOM resource hints.
/// This is a hint only; the browser retains control over actual scheduling.
pub(all) enum ResourceFetchPriority {
  Auto
  High
  Low
} derive(Eq)

///|
fn ResourceFetchPriority::to_string(self : ResourceFetchPriority) -> String {
  match self {
    Auto => "auto"
    High => "high"
    Low => "low"
  }
}

///|
/// Valid `as` destinations for `preload` according to React 19.2.
/// Select the destination that matches how the browser will consume the URL.
pub(all) enum PreloadDestination {
  Audio
  Document
  Embed
  Fetch
  Font
  Image
  Object
  Script
  Style
  Track
  Video
  Worker
} derive(Eq)

///|
fn PreloadDestination::to_string(self : PreloadDestination) -> String {
  match self {
    Audio => "audio"
    Document => "document"
    Embed => "embed"
    Fetch => "fetch"
    Font => "font"
    Image => "image"
    Object => "object"
    Script => "script"
    Style => "style"
    Track => "track"
    Video => "video"
    Worker => "worker"
  }
}

///|
/// Referrer policies accepted by React 19.2's `preload` options.
pub(all) enum ResourceReferrerPolicy {
  NoReferrerWhenDowngrade
  NoReferrer
  Origin
  OriginWhenCrossOrigin
  UnsafeUrl
} derive(Eq)

///|
fn ResourceReferrerPolicy::to_string(self : ResourceReferrerPolicy) -> String {
  match self {
    NoReferrerWhenDowngrade => "no-referrer-when-downgrade"
    NoReferrer => "no-referrer"
    Origin => "origin"
    OriginWhenCrossOrigin => "origin-when-cross-origin"
    UnsafeUrl => "unsafe-url"
  }
}

///|
/// Stylesheet precedence levels supported by React 19.2 `preinit`.
/// Higher levels are inserted so they can override lower-precedence styles.
pub(all) enum ResourceStylePrecedence {
  Reset
  Low
  Medium
  High
} derive(Eq)

///|
fn ResourceStylePrecedence::to_string(self : ResourceStylePrecedence) -> String {
  match self {
    Reset => "reset"
    Low => "low"
    Medium => "medium"
    High => "high"
  }
}

///|
fn optional_resource_string(value : String?) -> @dom.JsObscure {
  match value {
    Some(text) => @dom.JsObscure::from_string(text)
    None => @dom.JsObscure::null()
  }
}

///|
fn optional_cross_origin(value : ResourceCrossOrigin?) -> @dom.JsObscure {
  match value {
    Some(mode) => @dom.JsObscure::from_string(mode.to_string())
    None => @dom.JsObscure::null()
  }
}

///|
fn optional_fetch_priority(value : ResourceFetchPriority?) -> @dom.JsObscure {
  match value {
    Some(priority) => @dom.JsObscure::from_string(priority.to_string())
    None => @dom.JsObscure::null()
  }
}

///|
/// Typed options for React DOM `preload`.
/// Image-only fields are meaningful only with `PreloadDestination::Image`.
struct PreloadOptions {
  js_value : @dom.JsObscure
}

///|
extern "js" fn react_preload_options(
  destination : String,
  cross_origin : @dom.JsObscure,
  referrer_policy : @dom.JsObscure,
  integrity : @dom.JsObscure,
  mime_type : @dom.JsObscure,
  nonce : @dom.JsObscure,
  fetch_priority : @dom.JsObscure,
  image_src_set : @dom.JsObscure,
  image_sizes : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (as, crossOrigin, referrerPolicy, integrity, type, nonce, fetchPriority, imageSrcSet, imageSizes) => {
  #|   const options = { as };
  #|   if (crossOrigin != null) options.crossOrigin = crossOrigin;
  #|   if (referrerPolicy != null) options.referrerPolicy = referrerPolicy;
  #|   if (integrity != null) options.integrity = integrity;
  #|   if (type != null) options.type = type;
  #|   if (nonce != null) options.nonce = nonce;
  #|   if (fetchPriority != null) options.fetchPriority = fetchPriority;
  #|   if (imageSrcSet != null) options.imageSrcSet = imageSrcSet;
  #|   if (imageSizes != null) options.imageSizes = imageSizes;
  #|   return options;
  #| }

///|
/// Creates options for `preload`.
///
/// `destination` is required by React. `Fetch` resources must also provide a
/// `cross_origin` policy. `image_src_set` and `image_sizes` apply only to
/// `Image`; equivalent image hints are deduplicated by URL, source set, and
/// sizes, while other destinations are deduplicated by URL. Construction
/// aborts when `Fetch` is selected without `cross_origin`.
pub fn PreloadOptions::new(
  destination : PreloadDestination,
  cross_origin? : ResourceCrossOrigin,
  referrer_policy? : ResourceReferrerPolicy,
  integrity? : String,
  mime_type? : String,
  nonce? : String,
  fetch_priority? : ResourceFetchPriority,
  image_src_set? : String,
  image_sizes? : String,
) -> PreloadOptions {
  if destination == PreloadDestination::Fetch && cross_origin is None {
    abort("React preload with destination Fetch requires cross_origin")
  }
  let js_referrer_policy = match referrer_policy {
    Some(policy) => @dom.JsObscure::from_string(policy.to_string())
    None => @dom.JsObscure::null()
  }
  PreloadOptions::{
    js_value: react_preload_options(
      destination.to_string(),
      optional_cross_origin(cross_origin),
      js_referrer_policy,
      optional_resource_string(integrity),
      optional_resource_string(mime_type),
      optional_resource_string(nonce),
      optional_fetch_priority(fetch_priority),
      optional_resource_string(image_src_set),
      optional_resource_string(image_sizes),
    ),
  }
}

///|
/// Shared typed options for ESM module preload and preinit hints.
/// React always receives `as: "script"`; callers configure only CORS and
/// security metadata.
struct ModuleHintOptions {
  js_value : @dom.JsObscure
}

///|
extern "js" fn react_module_hint_options(
  cross_origin : @dom.JsObscure,
  integrity : @dom.JsObscure,
  nonce : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (crossOrigin, integrity, nonce) => {
  #|   const options = { as: "script" };
  #|   if (crossOrigin != null) options.crossOrigin = crossOrigin;
  #|   if (integrity != null) options.integrity = integrity;
  #|   if (nonce != null) options.nonce = nonce;
  #|   return options;
  #| }

///|
/// Creates ESM resource-hint options shared by `preload_module` and
/// `preinit_module`. Repeated calls with the same URL are deduplicated by
/// React, regardless of how many times the wrapper is invoked.
pub fn ModuleHintOptions::new(
  cross_origin? : ResourceCrossOrigin,
  integrity? : String,
  nonce? : String,
) -> ModuleHintOptions {
  ModuleHintOptions::{
    js_value: react_module_hint_options(
      optional_cross_origin(cross_origin),
      optional_resource_string(integrity),
      optional_resource_string(nonce),
    ),
  }
}

///|
/// Typed options for React DOM `preinit` of classic scripts or stylesheets.
/// Use the dedicated constructors so stylesheet precedence cannot be omitted.
struct PreinitOptions {
  js_value : @dom.JsObscure
}

///|
extern "js" fn react_preinit_options(
  resource_type : String,
  precedence : @dom.JsObscure,
  cross_origin : @dom.JsObscure,
  integrity : @dom.JsObscure,
  nonce : @dom.JsObscure,
  fetch_priority : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (as, precedence, crossOrigin, integrity, nonce, fetchPriority) => {
  #|   const options = { as };
  #|   if (precedence != null) options.precedence = precedence;
  #|   if (crossOrigin != null) options.crossOrigin = crossOrigin;
  #|   if (integrity != null) options.integrity = integrity;
  #|   if (nonce != null) options.nonce = nonce;
  #|   if (fetchPriority != null) options.fetchPriority = fetchPriority;
  #|   return options;
  #| }

///|
/// Creates `preinit` options for a classic external script. The resource is
/// fetched and executed when ready; use `preload` when execution should wait.
pub fn PreinitOptions::script(
  cross_origin? : ResourceCrossOrigin,
  integrity? : String,
  nonce? : String,
  fetch_priority? : ResourceFetchPriority,
) -> PreinitOptions {
  PreinitOptions::{
    js_value: react_preinit_options(
      "script",
      @dom.JsObscure::null(),
      optional_cross_origin(cross_origin),
      optional_resource_string(integrity),
      optional_resource_string(nonce),
      optional_fetch_priority(fetch_priority),
    ),
  }
}

///|
/// Creates `preinit` options for a stylesheet. `precedence` is mandatory and
/// determines its order relative to other React-managed stylesheets.
pub fn PreinitOptions::style(
  precedence : ResourceStylePrecedence,
  cross_origin? : ResourceCrossOrigin,
  integrity? : String,
  nonce? : String,
  fetch_priority? : ResourceFetchPriority,
) -> PreinitOptions {
  PreinitOptions::{
    js_value: react_preinit_options(
      "style",
      @dom.JsObscure::from_string(precedence.to_string()),
      optional_cross_origin(cross_origin),
      optional_resource_string(integrity),
      optional_resource_string(nonce),
      optional_fetch_priority(fetch_priority),
    ),
  }
}

///|
extern "js" fn react_flush_sync(callback : () -> Unit) -> Unit =
  #| (callback) => {
  #|   globalThis.ReactDOM.flushSync(() => {
  #|     callback();
  #|   });
  #| }

///|
/// Forces React to apply updates scheduled inside `callback` before returning.
///
/// This is a last-resort escape hatch for third-party or browser integrations
/// that must observe the updated DOM synchronously. It can hurt performance,
/// flush pending work outside the callback, run pending Effects, or reveal
/// Suspense fallbacks. Do not call it during render or an Effect.
pub fn flush_sync(callback : () -> Unit) -> Unit {
  react_flush_sync(callback)
}

///|
extern "js" fn react_prefetch_dns(href : String) -> Unit =
  #| (href) => { globalThis.ReactDOM.prefetchDNS(href); }

///|
/// Hints that the browser may resolve the host in `href` ahead of use.
/// Equivalent calls for the same server are deduplicated by React. During SSR,
/// call this only while rendering or in async work originating from rendering.
pub fn prefetch_dns(href : String) -> Unit {
  react_prefetch_dns(href)
}

///|
extern "js" fn react_preconnect(
  href : String,
  cross_origin : @dom.JsObscure,
) -> Unit =
  #| (href, crossOrigin) => {
  #|   globalThis.ReactDOM.preconnect(
  #|     href,
  #|     crossOrigin == null ? undefined : { crossOrigin },
  #|   );
  #| }

///|
/// Hints that the browser may open an early connection to the server in
/// `href`. Use `cross_origin` when the eventual request uses CORS. Equivalent
/// calls for the same server have the effect of one call.
pub fn preconnect(href : String, cross_origin? : ResourceCrossOrigin) -> Unit {
  react_preconnect(href, optional_cross_origin(cross_origin))
}

///|
extern "js" fn react_preload(href : String, options : @dom.JsObscure) -> Unit =
  #| (href, options) => { globalThis.ReactDOM.preload(href, options); }

///|
/// Hints that the browser should start downloading `href` with the configured
/// destination and metadata. Prefer framework resource management when one is
/// present; frameworks commonly emit and deduplicate these hints themselves.
pub fn preload(href : String, options : PreloadOptions) -> Unit {
  react_preload(href, options.js_value)
}

///|
extern "js" fn react_preload_module(
  href : String,
  options : @dom.JsObscure,
) -> Unit =
  #| (href, options) => { globalThis.ReactDOM.preloadModule(href, options); }

///|
/// Hints that the browser should download the ESM module at `href` without
/// evaluating it. Use `preinit_module` when it should execute when ready.
pub fn preload_module(href : String, options? : ModuleHintOptions) -> Unit {
  let js_options = match options {
    Some(value) => value.js_value
    None =>
      react_module_hint_options(
        @dom.JsObscure::null(),
        @dom.JsObscure::null(),
        @dom.JsObscure::null(),
      )
  }
  react_preload_module(href, js_options)
}

///|
extern "js" fn react_preinit(href : String, options : @dom.JsObscure) -> Unit =
  #| (href, options) => { globalThis.ReactDOM.preinit(href, options); }

///|
/// Hints that the browser should download and immediately apply a stylesheet
/// or execute a classic script. Select the matching `PreinitOptions`
/// constructor; use `preload` when the resource must not take effect yet.
pub fn preinit(href : String, options : PreinitOptions) -> Unit {
  react_preinit(href, options.js_value)
}

///|
extern "js" fn react_preinit_module(
  href : String,
  options : @dom.JsObscure,
) -> Unit =
  #| (href, options) => { globalThis.ReactDOM.preinitModule(href, options); }

///|
/// Hints that the browser should download and evaluate the ESM module at
/// `href`. Use `preload_module` when evaluation should wait. Framework-managed
/// applications usually do not need to call this API directly.
pub fn preinit_module(href : String, options? : ModuleHintOptions) -> Unit {
  let js_options = match options {
    Some(value) => value.js_value
    None =>
      react_module_hint_options(
        @dom.JsObscure::null(),
        @dom.JsObscure::null(),
        @dom.JsObscure::null(),
      )
  }
  react_preinit_module(href, js_options)
}

///|
extern "js" fn react_render_to_string(
  vdom : @dom.JsObscure,
  identifier_prefix : @dom.JsObscure,
) -> String =
  #| (vdom, identifierPrefix) => globalThis.ReactDOMServer.renderToString(
  #|   vdom,
  #|   identifierPrefix == null ? undefined : { identifierPrefix },
  #| )

///|
/// Options for React 19.2 Web Streams server rendering.
struct StreamRenderOptions {
  js_value : @dom.JsObscure
}

///|
extern "js" fn react_stream_render_options(
  identifier_prefix : @dom.JsObscure,
  bootstrap_scripts : FixedArray[String],
  bootstrap_modules : FixedArray[String],
  nonce : @dom.JsObscure,
  on_error : @dom.JsObscure,
) -> @dom.JsObscure =
  #| (identifierPrefix, bootstrapScripts, bootstrapModules, nonce, onError) => {
  #|   const options = {};
  #|   if (identifierPrefix != null) options.identifierPrefix = identifierPrefix;
  #|   if (bootstrapScripts.length > 0) options.bootstrapScripts = bootstrapScripts;
  #|   if (bootstrapModules.length > 0) options.bootstrapModules = bootstrapModules;
  #|   if (nonce != null) options.nonce = nonce;
  #|   if (onError != null) {
  #|     options.onError = (error) => {
  #|       onError(error);
  #|       return undefined;
  #|     };
  #|   }
  #|   return options;
  #| }

///|
/// Creates options for `render_to_readable_stream`.
/// `identifier_prefix` must match the client `RootOptions` during hydration.
pub fn StreamRenderOptions::new(
  identifier_prefix? : String,
  bootstrap_scripts? : Array[String] = [],
  bootstrap_modules? : Array[String] = [],
  nonce? : String,
  on_error? : (ReactError) -> Unit,
) -> StreamRenderOptions {
  let prefix = match identifier_prefix {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  let js_nonce = match nonce {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  let js_on_error = match on_error {
    Some(callback) => @dom.v_to_js_obscure(callback)
    None => @dom.JsObscure::null()
  }
  StreamRenderOptions::{
    js_value: react_stream_render_options(
      prefix,
      FixedArray::from_array(bootstrap_scripts),
      FixedArray::from_array(bootstrap_modules),
      js_nonce,
      js_on_error,
    ),
  }
}

///|
/// A React 19.2 server-rendered Web Stream and its abort controller.
/// Reading the stream transfers its one-shot body ownership to the consumer.
struct ReactReadableStream {
  js_value : @dom.JsObscure
}

///|
extern "js" fn react_render_to_readable_stream(
  vdom : @dom.JsObscure,
  options : @dom.JsObscure,
) -> @js_async.Promise[@dom.JsObscure] =
  #| async (vdom, options) => {
  #|   const controller = new AbortController();
  #|   const stream = await globalThis.ReactDOMServer.renderToReadableStream(
  #|     vdom,
  #|     { ...(options ?? {}), signal: controller.signal },
  #|   );
  #|   return { stream, controller };
  #| }

///|
extern "js" fn react_readable_stream_all_ready(
  handle : @dom.JsObscure,
) -> @js_async.Promise[Unit] =
  #| (handle) => handle.stream.allReady.then(() => undefined)

///|
extern "js" fn react_readable_stream_text(
  handle : @dom.JsObscure,
) -> @js_async.Promise[String] =
  #| (handle) => new Response(handle.stream).text()

///|
extern "js" fn react_readable_stream_abort(
  handle : @dom.JsObscure,
  reason : @dom.JsObscure,
) -> Unit =
  #| (handle, reason) => handle.controller.abort(reason ?? undefined)

///|
extern "js" fn react_readable_stream_value(
  handle : @dom.JsObscure,
) -> @js_async.JsReadableStream =
  #| (handle) => handle.stream

///|
/// Starts React 19.2 Web Streams rendering and resolves as soon as the shell is
/// ready. A shell failure rejects this async call.
pub async fn render_to_readable_stream(
  vdom : VirtualNode,
  options? : StreamRenderOptions,
) -> ReactReadableStream {
  let js_options = match options {
    Some(value) => value.js_value
    None => @dom.JsObscure::null()
  }
  let handle = react_render_to_readable_stream(vdom.to_js_obscure(), js_options).wait()
  ReactReadableStream::{ js_value: handle }
}

///|
/// Waits until the shell and every suspended boundary are ready. Use this for
/// crawlers or static generation when progressive delivery is unnecessary.
pub async fn ReactReadableStream::wait_all_ready(
  self : ReactReadableStream,
) -> Unit {
  react_readable_stream_all_ready(self.js_value).wait()
}

///|
/// Consumes the entire stream as UTF-8 HTML. A stream body can be consumed only
/// once; use `to_js_readable_stream` for direct Web API integration instead.
pub async fn ReactReadableStream::read_text(
  self : ReactReadableStream,
) -> String {
  react_readable_stream_text(self.js_value).wait()
}

///|
/// Aborts pending server work. React emits the nearest Suspense fallbacks and
/// leaves unfinished content for the client to render.
pub fn ReactReadableStream::abort(
  self : ReactReadableStream,
  reason? : String,
) -> Unit {
  let js_reason = match reason {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  react_readable_stream_abort(self.js_value, js_reason)
}

///|
/// Returns the underlying one-shot Web `ReadableStream` for a Response or other
/// JavaScript server-runtime integration.
pub fn ReactReadableStream::to_js_readable_stream(
  self : ReactReadableStream,
) -> @js_async.JsReadableStream {
  react_readable_stream_value(self.js_value)
}

///|
/// Renders a virtual DOM node to the specified parent element. Repeated calls
/// for the same parent reuse its React root.
pub fn render(vdom : VirtualNode, parent : @dom.Element) -> Unit {
  react_render(
    vdom.to_js_obscure(),
    parent.reinterpret_as_node(),
    @dom.JsObscure::null(),
  )
}

///|
/// Renders through a new root configured with React 19 root options. Options
/// are consumed only when this call creates the root; later renders reuse it.
pub fn render_with_options(
  vdom : VirtualNode,
  parent : @dom.Element,
  options : RootOptions,
) -> Unit {
  react_render(
    vdom.to_js_obscure(),
    parent.reinterpret_as_node(),
    options.to_js_obscure(),
  )
}

///|
/// Hydrates React-generated HTML already present in `parent`. The initial VDOM
/// must produce identical markup. Later `render` calls reuse the hydrated root.
pub fn hydrate_root(
  vdom : VirtualNode,
  parent : @dom.Element,
  options? : RootOptions,
) -> Unit {
  let js_options = match options {
    Some(value) => value.to_js_obscure()
    None => @dom.JsObscure::null()
  }
  react_hydrate_root(
    vdom.to_js_obscure(),
    parent.reinterpret_as_node(),
    js_options,
  )
}

///|
/// Creates a React portal whose DOM is placed in `parent` while context and
/// event propagation continue to follow the owning React tree.
pub fn create_portal(
  child : VirtualNode,
  parent : @dom.Element,
  key? : String,
) -> VirtualNode {
  let js_key = match key {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  JsNode(
    react_create_portal(
      child.to_js_obscure(),
      parent.reinterpret_as_node(),
      js_key,
    ),
  )
}

///|
/// Renders a React tree to an HTML string for basic synchronous SSR or SSG.
/// This intentionally does not provide streaming or wait for suspended data.
pub fn render_to_string(
  vdom : VirtualNode,
  identifier_prefix? : String,
) -> String {
  let js_prefix = match identifier_prefix {
    Some(value) => @dom.JsObscure::from_string(value)
    None => @dom.JsObscure::null()
  }
  react_render_to_string(vdom.to_js_obscure(), js_prefix)
}

///|
/// Unmounts the React root associated with a parent element, if one exists.
/// A later call to `render` or `hydrate_root` creates a fresh root.
pub fn unmount(parent : @dom.Element) -> Unit {
  react_unmount(parent.reinterpret_as_node())
}