Skip to content

DOM Node APIs#844

Merged
Freddy03h merged 10 commits intomainfrom
dom-node-api
Apr 10, 2026
Merged

DOM Node APIs#844
Freddy03h merged 10 commits intomainfrom
dom-node-api

Conversation

@Freddy03h
Copy link
Copy Markdown
Member

Blog Post : Starting from React Native 0.82, native components will provide DOM-like nodes via refs.
Documentation : Nodes from refs


I tried to follow the approach used in experimental-rescript-webapi but I hope this rescript feature for tagged variants can be addressed soon, as it would provide a better DX.

In the meantime, I added a NodeTypeHelper module to help with type conversions.
I know it doesn’t follow the zero-runtime binding rule, but while experimenting with experimental-rescript-webapi, I found this kind of helper quite useful, and it would otherwise have to be reimplemented in every project.

@Freddy03h
Copy link
Copy Markdown
Member Author

Freddy03h commented Mar 10, 2026

Migration should be easy if the rescript feature for tagged variants is accepted.

Example with the current PR

let textData = switch element.childNodes
->DOMApi.NodeList.item(0)
->Null.map(node => node->DOMApi.NodeTypeHelper.findNodeType) {
| Null.Value(Text(text)) => text.data
| _ => "not a text node"
}

Example with the rescript feature for tagged variants.

let textData = switch element.childNodes->DOMApi.NodeList.item(0) {
| Null.Value(Text(text)) => text.data
| _ => "not a text node"
}

@fhammerschmidt fhammerschmidt requested a review from cknitt March 12, 2026 16:36

module Document = {
include Node.Impl({
type t = element
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't that be

Suggested change
type t = element
type t = document

?

| Document(document)
| Unknown

let findNodeType = (node: node) =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could be called classify like similar functions in the stdlib. And the module could be NodeType. So that you would have NodeType.classify returning a NodeType.t.

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Mar 18, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c58b72837

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

type t = element
})

include DOMAPI.Element.Impl({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not expose DOM ref APIs to supported 0.81 apps

NativeElement now includes the new DOM-style methods on every ref, but this package still advertises react-native >=0.81.0 support. React Native only started returning DOM-like nodes from refs in 0.82, so an app that is still on 0.81 can compile against these bindings and then hit undefined is not a function at runtime as soon as it calls one of the newly exposed methods.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It will be done before release

@send
external compareDocumentPosition: (T.t, node) => int = "compareDocumentPosition"
@send external contains: (T.t, node) => bool = "contains"
@send external getRootNode: T.t => T.t = "getRootNode"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Return a node from getRootNode

getRootNode() is typed as returning T.t, but for mounted element and text nodes the root is the document node, not another element/text node. The current signature is only correct for the detached-node case mentioned in the React Native docs, so common mounted calls will be typed as element/text and let callers use subtype-specific fields on a document object at runtime.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should be node

type t = document
})

@send external getElementById: (document, string) => element = "getElementById"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make getElementById nullable

getElementById() can legitimately miss when no native component has the requested id/nativeID, but this binding models it as always returning element. That makes the absence case impossible to express in ReScript and turns a normal lookup miss into a null dereference when consumers immediately access fields or methods on the result.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

good catch, I'll also open a PR on webapi

type t = text
})

@send external substringData: (text, ~offset: int, ~count: int) => unit = "substringData"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return the substring from Text.substringData

substringData() returns the extracted text, but the external is declared as unit. As written, the new Text-node binding silently discards React Native's return value, so callers cannot actually read any substring from a text node through this API.

Useful? React with 👍 / 👎.

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Mar 18, 2026

@Freddy03h I think Codex has some good points here, can you check them?

@Freddy03h Freddy03h marked this pull request as ready for review March 18, 2026 19:15
cknitt
cknitt previously approved these changes Mar 19, 2026
@cknitt cknitt mentioned this pull request Mar 19, 2026
@Freddy03h
Copy link
Copy Markdown
Member Author

I have a concern:

// NativeElement.res
type element = DOMAPI.element

// ScrollViewElement.res
type element

If I change ScrollView’s element to DOMAPI.element, it means we could use a ScrollView method on a View.
Is there a way to prevent that? Thanks.

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Mar 20, 2026

That is a good point.

I think we need to keep the element types abstract to avoid this issue, and offer "casts" using %identity, like

scrollViewElement->ScrollViewElement.asNativeElement->NativeElement.asDOMElement

and/or directly

scrollViewElement->ScrollViewElement.asDOMElement

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 6, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7346c82edd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +13 to +14
include DOMAPI.Element.Impl({
type t = element
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate DOM node APIs to React Native 0.82+

NativeElement.Impl now mixes in DOMAPI.Element.Impl for every native ref, which makes DOM node fields/methods available in all consuming apps even though this package still declares support for React Native >=0.81. The DOM node API was introduced in 0.82, so 0.81 consumers can compile calls like nodeType/childNodes from these bindings and then hit undefined at runtime. Please either raise the minimum supported RN version in this change set or expose these bindings behind a 0.82-specific surface.

Useful? React with 👍 / 👎.

parentElement: Js.Null.t<'element>,
parentNode: Js.Null.t<'node>,
previousSibling: Js.Null.t<'node>,
textContent: string,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make Node.textContent nullable in DOM node bindings

readOnlyNode.textContent is typed as non-null string, but these bindings are explicitly modeled as web-compatible Node APIs, and Document nodes can return null for textContent. With the current type, callers may treat document.textContent as always present and crash when RN returns null. This field should be Js.Null.t<string> (or equivalent) to match runtime behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

According to the TypeScript types, it shouldn’t be null, but in my React Native tests it was indeed null for document.

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 6, 2026

We need to make clear that this is a breaking change. I tested the bindings against a project of ours and had to make some adjustments to get it to compile, as it contained bindings for other React Native libs where I was directly using NativeElement.ref, for example.

Also, see the hint from Codex regarding node.textContent.

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 6, 2026

I think we should release a beta version for broader testing first before doing a release version.

@Freddy03h
Copy link
Copy Markdown
Member Author

Freddy03h commented Apr 6, 2026

What about adding this in NativeElement?

@deprecated(
  "Bindings maintainers: please use NativeElement.Impl instead of accessing the types directly."
)
include Impl({
  type t = DOMAPI.anyElement
})

I’m not sure if deprecation will work in this situation. Maybe we could just keep the existing code and mark each type as deprecated?

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 6, 2026

I’m not sure if deprecation will work in this situation. Maybe we could just keep the existing code and mark each type as deprecated?

If you add that, I can retest to see if my project can build without any changes then.

Something else, about all those

external asButtonElement: DOMAPI.anyElement => element = "%identity"

etc. - these are actually unsafe (as not every element is a button element). So maybe these should be called unsafeAsButtonElement etc.?

But actually, couldn't this be part of NativeElement.Impl as unsafeFromAnyElement (or something like that)?

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 7, 2026

Nice! Retested, now the breaking changes for native element handling are gone.

Not really related to this PR, but there is one remaining change that was breaking for me, the change of the return type of AppRegistry.registerComponent to string.
This function just returns the app key that was passed in which does not bring much value, I am wondering if we should revert this change to the binding.

@Freddy03h
Copy link
Copy Markdown
Member Author

Okay! But did you see the deprecation warning for NativeElement.ref?

@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 10, 2026

No, didn't get the warnings.

@Freddy03h Freddy03h requested a review from cknitt April 10, 2026 08:24
@Freddy03h Freddy03h merged commit e5bebc6 into main Apr 10, 2026
2 checks passed
@Freddy03h Freddy03h deleted the dom-node-api branch April 10, 2026 09:09
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.

2 participants