diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 3d8cebe206..6c9ecf2933 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Attempt form submission when pressing `Enter` on the `` component ([#2972](https://github.com/tailwindlabs/headlessui/pull/2972))
- Make the `Combobox` component `nullable` by default ([#3064](https://github.com/tailwindlabs/headlessui/pull/3064))
- Deprecate the `entered` prop on the `Transition` component ([#3089](https://github.com/tailwindlabs/headlessui/pull/3089))
+- Use native `useId` and `useSyncExternalStore` hooks ([#3092](https://github.com/tailwindlabs/headlessui/pull/3092))
### Added
diff --git a/packages/@headlessui-react/src/hooks/use-id.ts b/packages/@headlessui-react/src/hooks/use-id.ts
index 8b18bf7b13..1003f0bfbf 100644
--- a/packages/@headlessui-react/src/hooks/use-id.ts
+++ b/packages/@headlessui-react/src/hooks/use-id.ts
@@ -1,24 +1,2 @@
-import React from 'react'
-import { env } from '../utils/env'
-import { useIsoMorphicEffect } from './use-iso-morphic-effect'
-import { useServerHandoffComplete } from './use-server-handoff-complete'
-
-// We used a "simple" approach first which worked for SSR and rehydration on the client. However we
-// didn't take care of the Suspense case. To fix this we used the approach the @reach-ui/auto-id
-// uses.
-//
-// Credits: https://github.com/reach/reach-ui/blob/develop/packages/auto-id/src/index.tsx
-
-export let useId =
- // Prefer React's `useId` if it's available.
- React.useId ??
- function useId() {
- let ready = useServerHandoffComplete()
- let [id, setId] = React.useState(ready ? () => env.nextId() : null)
-
- useIsoMorphicEffect(() => {
- if (id === null) setId(env.nextId())
- }, [id])
-
- return id != null ? '' + id : undefined
- }
+// Re-exporting the useId hook such that we can easily mock this hook in tests.
+export { useId } from 'react'
diff --git a/packages/@headlessui-react/src/hooks/use-store.ts b/packages/@headlessui-react/src/hooks/use-store.ts
index 25f14ef7b5..f625d9194e 100644
--- a/packages/@headlessui-react/src/hooks/use-store.ts
+++ b/packages/@headlessui-react/src/hooks/use-store.ts
@@ -1,4 +1,4 @@
-import { useSyncExternalStore } from '../use-sync-external-store-shim/index'
+import { useSyncExternalStore } from 'react'
import type { Store } from '../utils/store'
export function useStore(store: Store) {
diff --git a/packages/@headlessui-react/src/use-sync-external-store-shim/index.ts b/packages/@headlessui-react/src/use-sync-external-store-shim/index.ts
deleted file mode 100644
index 28a6609e74..0000000000
--- a/packages/@headlessui-react/src/use-sync-external-store-shim/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-// This was taken from the ESM / CJS compatible version found in Remix Router:
-// https://github.com/remix-run/react-router/tree/43cc1aacd8b132507618a4a1dd7de3674cd7bcf4/packages/react-router/lib/use-sync-external-store-shim
-
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- */
-
-import * as React from 'react'
-import { useSyncExternalStore as client } from './useSyncExternalStoreShimClient'
-import { useSyncExternalStore as server } from './useSyncExternalStoreShimServer'
-
-const canUseDOM: boolean = !!(
- typeof window !== 'undefined' &&
- typeof window.document !== 'undefined' &&
- typeof window.document.createElement !== 'undefined'
-)
-
-const isServerEnvironment = !canUseDOM
-const shim = isServerEnvironment ? server : client
-
-type UseSyncExternalStoreFn = (
- subscribe: (fn: () => void) => () => void,
- getSnapshot: () => T,
- // Note: The shim does not use getServerSnapshot, because pre-18 versions of
- // React do not expose a way to check if we're hydrating. So users of the shim
- // will need to track that themselves and return the correct value
- // from `getSnapshot`.
- getServerSnapshot?: () => T
-) => T
-
-// @ts-ignore
-export const useSyncExternalStore: UseSyncExternalStoreFn =
- 'useSyncExternalStore' in React ? ((r) => r.useSyncExternalStore)(React) : shim
diff --git a/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts b/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts
deleted file mode 100644
index 2ebb849344..0000000000
--- a/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimClient.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-// @ts-nocheck
-
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import * as React from 'react'
-
-// Make typescript happy
-declare var __DEV__: boolean
-
-/**
- * inlined Object.is polyfill to avoid requiring consumers ship their own
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
- */
-function isPolyfill(x: any, y: any) {
- return (
- (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
- )
-}
-
-const is: (x: any, y: any) => boolean = typeof Object.is === 'function' ? Object.is : isPolyfill
-
-// Intentionally not using named imports because Rollup uses dynamic
-// dispatch for CommonJS interop named imports.
-const { useState, useEffect, useLayoutEffect, useDebugValue } = React
-
-let didWarnOld18Alpha = false
-let didWarnUncachedGetSnapshot = false
-
-// Disclaimer: This shim breaks many of the rules of React, and only works
-// because of a very particular set of implementation details and assumptions
-// -- change any one of them and it will break. The most important assumption
-// is that updates are always synchronous, because concurrent rendering is
-// only available in versions of React that also have a built-in
-// useSyncExternalStore API. And we only use this shim when the built-in API
-// does not exist.
-//
-// Do not assume that the clever hacks used by this hook also work in general.
-// The point of this shim is to replace the need for hacks by other libraries.
-export function useSyncExternalStore(
- subscribe: (fn: () => void) => () => void,
- getSnapshot: () => T,
- // Note: The shim does not use getServerSnapshot, because pre-18 versions of
- // React do not expose a way to check if we're hydrating. So users of the shim
- // will need to track that themselves and return the correct value
- // from `getSnapshot`.
- getServerSnapshot?: () => T
-): T {
- if (__DEV__) {
- if (!didWarnOld18Alpha) {
- if ('startTransition' in React) {
- didWarnOld18Alpha = true
- console.error(
- 'You are using an outdated, pre-release alpha of React 18 that ' +
- 'does not support useSyncExternalStore. The ' +
- 'use-sync-external-store shim will not work correctly. Upgrade ' +
- 'to a newer pre-release.'
- )
- }
- }
- }
-
- // Read the current snapshot from the store on every render. Again, this
- // breaks the rules of React, and only works here because of specific
- // implementation details, most importantly that updates are
- // always synchronous.
- const value = getSnapshot()
- if (__DEV__) {
- if (!didWarnUncachedGetSnapshot) {
- const cachedValue = getSnapshot()
- if (!is(value, cachedValue)) {
- console.error('The result of getSnapshot should be cached to avoid an infinite loop')
- didWarnUncachedGetSnapshot = true
- }
- }
- }
-
- // Because updates are synchronous, we don't queue them. Instead we force a
- // re-render whenever the subscribed state changes by updating an some
- // arbitrary useState hook. Then, during render, we call getSnapshot to read
- // the current value.
- //
- // Because we don't actually use the state returned by the useState hook, we
- // can save a bit of memory by storing other stuff in that slot.
- //
- // To implement the early bailout, we need to track some things on a mutable
- // object. Usually, we would put that in a useRef hook, but we can stash it in
- // our useState hook instead.
- //
- // To force a re-render, we call forceUpdate({inst}). That works because the
- // new object always fails an equality check.
- const [{ inst }, forceUpdate] = useState({ inst: { value, getSnapshot } })
-
- // Track the latest getSnapshot function with a ref. This needs to be updated
- // in the layout phase so we can access it during the tearing check that
- // happens on subscribe.
- useLayoutEffect(() => {
- inst.value = value
- inst.getSnapshot = getSnapshot
-
- // Whenever getSnapshot or subscribe changes, we need to check in the
- // commit phase if there was an interleaved mutation. In concurrent mode
- // this can happen all the time, but even in synchronous mode, an earlier
- // effect may have mutated the store.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({ inst })
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [subscribe, value, getSnapshot])
-
- useEffect(() => {
- // Check for changes right before subscribing. Subsequent changes will be
- // detected in the subscription handler.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({ inst })
- }
- const handleStoreChange = () => {
- // TODO: Because there is no cross-renderer API for batching updates, it's
- // up to the consumer of this library to wrap their subscription event
- // with unstable_batchedUpdates. Should we try to detect when this isn't
- // the case and print a warning in development?
-
- // The store changed. Check if the snapshot changed since the last time we
- // read from the store.
- if (checkIfSnapshotChanged(inst)) {
- // Force a re-render.
- forceUpdate({ inst })
- }
- }
- // Subscribe to the store and return a clean-up function.
- return subscribe(handleStoreChange)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [subscribe])
-
- useDebugValue(value)
- return value
-}
-
-function checkIfSnapshotChanged(inst: any) {
- const latestGetSnapshot = inst.getSnapshot
- const prevValue = inst.value
- try {
- const nextValue = latestGetSnapshot()
- return !is(prevValue, nextValue)
- } catch (error) {
- return true
- }
-}
diff --git a/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts b/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts
deleted file mode 100644
index 2d550daec5..0000000000
--- a/packages/@headlessui-react/src/use-sync-external-store-shim/useSyncExternalStoreShimServer.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// @ts-nocheck
-
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- */
-
-export function useSyncExternalStore(
- subscribe: (fn: () => void) => () => void,
- getSnapshot: () => T,
- getServerSnapshot?: () => T
-): T {
- // Note: The shim does not use getServerSnapshot, because pre-18 versions of
- // React do not expose a way to check if we're hydrating. So users of the shim
- // will need to track that themselves and return the correct value
- // from `getSnapshot`.
- return getSnapshot()
-}