Skip to content

React 18 support #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions src/React.res
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,60 @@ external useImperativeHandle7: (
('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useImperativeHandle"

@module("react") external useId: unit => string = "useId"

@module("react") external useDeferredValue: 'value => 'value = "useDeferredValue"

@module("react")
external useTransition: unit => (bool, (. unit => unit) => unit) = "useTransition"

@module("react")
external useInsertionEffect: (@uncurry (unit => option<unit => unit>)) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect0: (@uncurry (unit => option<unit => unit>), @as(json`[]`) _) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect1: (@uncurry (unit => option<unit => unit>), array<'a>) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect2: (@uncurry (unit => option<unit => unit>), ('a, 'b)) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect3: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c)) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect4: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c, 'd)) => unit =
"useInsertionEffect"
@module("react")
external useInsertionEffect5: (
@uncurry (unit => option<unit => unit>),
('a, 'b, 'c, 'd, 'e),
) => unit = "useInsertionEffect"
@module("react")
external useInsertionEffect6: (
@uncurry (unit => option<unit => unit>),
('a, 'b, 'c, 'd, 'e, 'f),
) => unit = "useInsertionEffect"
@module("react")
external useInsertionEffect7: (
@uncurry (unit => option<unit => unit>),
('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useInsertionEffect"

@module("react")
external useSyncExternalStore: (
@uncurry (unit => @uncurry (unit => unit)),
@uncurry unit => 'state,
) => 'state = "useSyncExternalStore"

@module("react")
external useSyncExternalStoreWithServerSnapshot: (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can bikeshed the name, it's a tricky one. The alternative could be to name the parameters so that we can have getServerSnapshot be optional so it doesn't need to passed?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is interesting to me because there are a bunch of potential future arguments - useSyncExternalStoreWithSelector in the discussion gets wildly long and adding an option to get named optional arguments means it could break in the future if those args get upstreamed into the React export. I think your naming make sense and is probably the safest way to work with these at least until there's more stability for 18 and we see how folks actually use this hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think one option could even be to leave the uSES hooks out for the time being. These hooks I expect to not be very prevalent at all in userland code, but I could be wrong 😅

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd name the arguments, as there are two unit => 'state callbacks.

@uncurry (unit => @uncurry (unit => unit)),
@uncurry unit => 'state,
@uncurry unit => 'state,
) => 'state = "useSyncExternalStore"

module Uncurried = {
@module("react")
external useState: (@uncurry (unit => 'state)) => ('state, (. 'state => 'state) => unit) =
Expand Down Expand Up @@ -436,14 +490,6 @@ module Uncurried = {
) => callback<'input, 'output> = "useCallback"
}

type transitionConfig = {timeoutMs: int}

@module("react")
external useTransition: (
~config: transitionConfig=?,
unit,
) => (callback<callback<unit, unit>, unit>, bool) = "useTransition"

@set
external setDisplayName: (component<'props>, string) => unit = "displayName"

Expand Down
16 changes: 8 additions & 8 deletions src/ReactDOM.res
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
external querySelector: string => option<Dom.element> = "document.querySelector"

@module("react-dom")
@deprecated("ReactDOM.render is no longer supported in React 18. Use createRoot instead.")
external render: (React.element, Dom.element) => unit = "render"

module Experimental = {
type root
module Root = {
type t

@module("react-dom")
external createRoot: Dom.element => root = "createRoot"
@send external render: (t, React.element) => unit = "render"

@module("react-dom")
external createBlockingRoot: Dom.element => root = "createBlockingRoot"

@send external render: (root, React.element) => unit = "render"
@send external unmount: (t, unit) => unit = "unmount"
}

@module("react-dom")
Copy link
Member

Choose a reason for hiding this comment

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

This should be react-dom/client.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cknitt I addressed this in the followup PR #46 which has been sitting in limbo for 2 months. I suggest we merge this PR and then open #46 for review, or maybe @tom-sherman can "steal" the commits in #46 and include them in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whatever works, just need a maintainer to get the ball rolling!

@rickyvetter Pls 🙏

external createRoot: Dom.element => Root.t = "createRoot"

@module("react-dom")
external hydrate: (React.element, Dom.element) => unit = "hydrate"

Expand Down
87 changes: 61 additions & 26 deletions test/React__test.res
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render DOM elements", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<div> {"Hello world!"->React.string} </div>, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<div> {"Hello world!"->React.string} </div>,
)
)

expect.bool(
container->DOM.findBySelectorAndTextContent("div", "Hello world!")->Option.isSome,
Expand All @@ -165,7 +169,7 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render null elements", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<div> React.null </div>, container))
act(() => ReactDOM.createRoot(container)->ReactDOM.Root.render(<div> React.null </div>))

expect.bool(
container->DOM.findBySelectorAndPartialTextContent("div", "")->Option.isSome,
Expand All @@ -175,7 +179,9 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render string elements", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<div> {"Hello"->React.string} </div>, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(<div> {"Hello"->React.string} </div>)
)

expect.bool(
container->DOM.findBySelectorAndPartialTextContent("div", "Hello")->Option.isSome,
Expand All @@ -185,7 +191,7 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render int elements", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<div> {12345->React.int} </div>, container))
act(() => ReactDOM.createRoot(container)->ReactDOM.Root.render(<div> {12345->React.int} </div>))

expect.bool(
container->DOM.findBySelectorAndPartialTextContent("div", "12345")->Option.isSome,
Expand All @@ -195,7 +201,9 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render float elements", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<div> {12.345->React.float} </div>, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(<div> {12.345->React.float} </div>)
)

expect.bool(
container->DOM.findBySelectorAndPartialTextContent("div", "12.345")->Option.isSome,
Expand All @@ -206,7 +214,9 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)
let array = [1, 2, 3]->Array.map(item => <div key=j`$item`> {item->React.int} </div>)

act(() => ReactDOM.render(<div> {array->React.array} </div>, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(<div> {array->React.array} </div>)
)

expect.bool(
container->DOM.findBySelectorAndPartialTextContent("div", "1")->Option.isSome,
Expand All @@ -225,9 +235,8 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)

act(() =>
ReactDOM.render(
ReactDOM.createRoot(container)->ReactDOM.Root.render(
React.cloneElement(<div> {"Hello"->React.string} </div>, {"data-name": "World"}),
container,
)
)

Expand All @@ -241,7 +250,7 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render react components", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<DummyStatefulComponent />, container))
act(() => ReactDOM.createRoot(container)->ReactDOM.Root.render(<DummyStatefulComponent />))

expect.bool(
container->DOM.findBySelectorAndTextContent("button", "0")->Option.isSome,
Expand All @@ -268,7 +277,7 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render react components with reducers", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<DummyReducerComponent />, container))
act(() => ReactDOM.createRoot(container)->ReactDOM.Root.render(<DummyReducerComponent />))

expect.bool(
container->DOM.findBySelectorAndTextContent(".value", "0")->Option.isSome,
Expand Down Expand Up @@ -312,7 +321,9 @@ describe("React", ({test, beforeEach, afterEach}) => {
test("can render react components with reducers (map state)", ({expect}) => {
let container = getContainer(container)

act(() => ReactDOM.render(<DummyReducerWithMapStateComponent />, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(<DummyReducerWithMapStateComponent />)
)

expect.bool(
container->DOM.findBySelectorAndTextContent(".value", "1")->Option.isSome,
Expand Down Expand Up @@ -357,9 +368,21 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)
let callback = Mock.fn()

act(() => ReactDOM.render(<DummyComponentWithEffect value=0 callback />, container))
act(() => ReactDOM.render(<DummyComponentWithEffect value=1 callback />, container))
act(() => ReactDOM.render(<DummyComponentWithEffect value=1 callback />, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithEffect value=0 callback />,
)
)
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithEffect value=1 callback />,
)
)
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithEffect value=1 callback />,
)
)

expect.value(callback->Mock.getMock->Mock.calls).toEqual([[0], [1]])
})
Expand All @@ -368,9 +391,21 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)
let callback = Mock.fn()

act(() => ReactDOM.render(<DummyComponentWithLayoutEffect value=0 callback />, container))
act(() => ReactDOM.render(<DummyComponentWithLayoutEffect value=1 callback />, container))
act(() => ReactDOM.render(<DummyComponentWithLayoutEffect value=1 callback />, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithLayoutEffect value=0 callback />,
)
)
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithLayoutEffect value=1 callback />,
)
)
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithLayoutEffect value=1 callback />,
)
)

expect.value(callback->Mock.getMock->Mock.calls).toEqual([[0], [1]])
})
Expand All @@ -387,7 +422,11 @@ describe("React", ({test, beforeEach, afterEach}) => {
let myRef = ref(None)
let callback = reactRef => myRef := Some(reactRef)

act(() => ReactDOM.render(<DummyComponentWithRefAndEffect callback />, container))
act(() =>
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentWithRefAndEffect callback />,
)
)

expect.value(myRef.contents->Option.map(item => item.current)).toEqual(Some(2))
})
Expand All @@ -396,11 +435,10 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)

act(() =>
ReactDOM.render(
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyComponentThatMapsChildren>
<div> {1->React.int} </div> <div> {2->React.int} </div> <div> {3->React.int} </div>
</DummyComponentThatMapsChildren>,
container,
)
)

Expand All @@ -421,9 +459,8 @@ describe("React", ({test, beforeEach, afterEach}) => {
let container = getContainer(container)

act(() =>
ReactDOM.render(
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<DummyContext.Provider value=10> <DummyContext.Consumer /> </DummyContext.Provider>,
container,
)
)

Expand All @@ -437,11 +474,10 @@ describe("React", ({test, beforeEach, afterEach}) => {
let value = ref("")

act(() =>
ReactDOM.render(
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<input
name="test-input" onChange={event => value := (event->ReactEvent.Form.target)["value"]}
/>,
container,
)
)

Expand All @@ -461,7 +497,7 @@ describe("React", ({test, beforeEach, afterEach}) => {
let consoleFn = Console.disableError()

act(() =>
ReactDOM.render(
ReactDOM.createRoot(container)->ReactDOM.Root.render(
<RescriptReactErrorBoundary
fallback={({error, info}) => {
switch error {
Expand All @@ -474,7 +510,6 @@ describe("React", ({test, beforeEach, afterEach}) => {
}}>
<ComponentThatThrows value=1 />
</RescriptReactErrorBoundary>,
container,
)
)

Expand Down