diff --git a/packages/core/index.ts b/packages/core/index.ts
index 8da1ef601d9..9c2770a5b6c 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -18,6 +18,7 @@ export * from './useBroadcastChannel'
export * from './useBrowserLocation'
export * from './useCached'
export * from './useClipboard'
+export * from './useCloned'
export * from './useColorMode'
export * from './useConfirmDialog'
export * from './useCssVar'
diff --git a/packages/core/useCloned/demo.vue b/packages/core/useCloned/demo.vue
new file mode 100644
index 00000000000..9246fb256fd
--- /dev/null
+++ b/packages/core/useCloned/demo.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/packages/core/useCloned/index.md b/packages/core/useCloned/index.md
new file mode 100644
index 00000000000..71e4b1cdd22
--- /dev/null
+++ b/packages/core/useCloned/index.md
@@ -0,0 +1,53 @@
+---
+category: Utilities
+---
+
+# useCloned
+
+Reactive clone of a ref. By default, it use `JSON.parse(JSON.stringify())` to do the clone.
+
+## Usage
+
+```ts
+import { useCloned } from '@vueuse/core'
+
+const original = ref({ key: 'value' })
+
+const { cloned } = useCloned(original)
+
+original.key = 'some new value'
+
+console.log(cloned.value.key) // 'some new value'
+```
+
+## Manual cloning
+
+```ts
+import { useCloned } from '@vueuse/core'
+
+const original = ref({ key: 'value' })
+
+const { cloned, sync } = useCloned(original, { manual: true })
+
+original.key = 'manual'
+
+console.log(cloned.value.key) // 'value'
+
+sync()
+
+console.log(cloned.value.key)// 'manual'
+```
+
+## Custom Clone Function
+
+Using [`klona`](https://www.npmjs.com/package/klona) for example:
+
+```ts
+import { useCloned } from '@vueuse/core'
+import { klona } from 'klona'
+
+const original = ref({ key: 'value' })
+
+const { cloned, sync } = useCloned(original, { clone: klona })
+```
+
diff --git a/packages/core/useCloned/index.test.ts b/packages/core/useCloned/index.test.ts
new file mode 100644
index 00000000000..dfb299f6539
--- /dev/null
+++ b/packages/core/useCloned/index.test.ts
@@ -0,0 +1,84 @@
+import { useCloned } from '@vueuse/core'
+import { expect } from 'vitest'
+import { nextTick, ref } from 'vue-demi'
+
+describe('useCloned', () => {
+ it('works with simple objects', () => {
+ const data = { test: 'test' }
+
+ const { cloned, sync } = useCloned(data)
+
+ expect(cloned.value).toEqual(data)
+
+ cloned.value = { test: 'failed' }
+
+ sync()
+
+ expect(cloned.value).toEqual(data)
+ })
+
+ it('works with refs', async () => {
+ const data = ref({ test: 'test' })
+
+ const { cloned } = useCloned(data)
+
+ data.value.test = 'success'
+
+ await nextTick()
+
+ expect(cloned.value).toEqual(data.value)
+ })
+
+ it('works with refs and manual sync', async () => {
+ const data = ref({ test: 'test' })
+
+ const { cloned, sync } = useCloned(data, { manual: true })
+
+ data.value.test = 'success'
+
+ expect(cloned.value).not.toEqual(data.value)
+
+ sync()
+
+ expect(cloned.value).toEqual(data.value)
+ })
+
+ it('works with custom clone function', async () => {
+ const data = ref>({ test: 'test' })
+
+ const { cloned } = useCloned(data, {
+ clone: source => ({ ...source, proxyTest: true }),
+ })
+
+ data.value.test = 'partial'
+
+ await nextTick()
+
+ expect(cloned.value.test).toBe('partial')
+ expect(cloned.value.proxyTest).toBe(true)
+ })
+
+ it('works with watch options', async () => {
+ const data = ref({ test: 'test' })
+
+ const { cloned } = useCloned(data, { immediate: false, deep: false })
+
+ await nextTick()
+
+ // test immediate: false
+ expect(cloned.value).toEqual({})
+
+ data.value.test = 'not valid'
+
+ await nextTick()
+
+ // test deep: false
+ expect(cloned.value).toEqual({})
+
+ data.value = { test: 'valid' }
+
+ await nextTick()
+
+ expect(cloned.value).toEqual(data.value)
+ })
+})
diff --git a/packages/core/useCloned/index.ts b/packages/core/useCloned/index.ts
new file mode 100644
index 00000000000..7c95cacd01d
--- /dev/null
+++ b/packages/core/useCloned/index.ts
@@ -0,0 +1,65 @@
+import type { MaybeComputedRef } from '@vueuse/shared'
+import type { ComputedRef, WatchOptions } from 'vue-demi'
+import { isRef, ref, unref, watch } from 'vue-demi'
+
+export interface UseClonedOptions extends WatchOptions {
+ /**
+ * Custom clone function.
+ *
+ * By default, it use `JSON.parse(JSON.stringify(value))` to clone.
+ */
+ clone?: (source: T) => T
+
+ /**
+ * Manually sync the ref
+ *
+ * @default false
+ */
+ manual?: boolean
+}
+
+export interface UseClonedReturn {
+ /**
+ * Cloned ref
+ */
+ cloned: ComputedRef
+ /**
+ * Sync cloned data with source manually
+ */
+ sync: () => void
+}
+
+function cloneFnJSON(source: T): T {
+ return JSON.parse(JSON.stringify(source))
+}
+
+export function useCloned(
+ source: MaybeComputedRef,
+ options: UseClonedOptions = {},
+) {
+ const cloned = ref({} as T)
+ const {
+ manual,
+ clone = cloneFnJSON,
+ // watch options
+ deep = true,
+ immediate = true,
+ } = options
+
+ function sync() {
+ cloned.value = clone(unref(source))
+ }
+
+ if (!manual && isRef(source)) {
+ watch(source, sync, {
+ ...options,
+ deep,
+ immediate,
+ })
+ }
+ else {
+ sync()
+ }
+
+ return { cloned, sync }
+}