Skip to content
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

[v2] breaking: do not throw promises #813

Merged
merged 8 commits into from
Nov 12, 2023
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.0.3",
"proxy-memoize": "^2.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "18.3.0-canary-c47c306a7-20231109",
"react-dom": "18.3.0-canary-c47c306a7-20231109",
"redux": "^4.2.1",
"rollup": "^4.2.0",
"rollup-plugin-esbuild": "^6.1.0",
Expand Down
15 changes: 3 additions & 12 deletions src/react.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
/// <reference types="react/experimental" />

import ReactExports, {
useCallback,
useDebugValue,
useEffect,
useMemo,
useRef,
} from 'react'
import { useCallback, useDebugValue, useEffect, useMemo, useRef } from 'react'
import {
affectedToPathList,
createProxy as createProxyToCompare,
Expand All @@ -21,7 +13,6 @@ import useSyncExternalStoreExports from 'use-sync-external-store/shim'
import { snapshot, subscribe } from './vanilla.ts'
import type { INTERNAL_Snapshot as Snapshot } from './vanilla.ts'

const { use } = ReactExports
const { useSyncExternalStore } = useSyncExternalStoreExports

const useAffectedDebugValue = (
Expand Down Expand Up @@ -133,7 +124,7 @@ export function useSnapshot<T extends object>(
[proxyObject, notifyInSync]
),
() => {
const nextSnapshot = snapshot(proxyObject, use)
const nextSnapshot = snapshot(proxyObject)
try {
if (
!inRender &&
Expand All @@ -154,7 +145,7 @@ export function useSnapshot<T extends object>(
}
return nextSnapshot
},
() => snapshot(proxyObject, use)
() => snapshot(proxyObject)
)
inRender = false
const currAffected = new WeakMap()
Expand Down
47 changes: 5 additions & 42 deletions src/vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ type SnapshotIgnore =

type Snapshot<T> = T extends SnapshotIgnore
? T
: T extends Promise<unknown>
? Awaited<T>
: T extends object
? { readonly [K in keyof T]: Snapshot<T[K]> }
: T
Expand All @@ -45,13 +43,7 @@ type Snapshot<T> = T extends SnapshotIgnore
*/
export type INTERNAL_Snapshot<T> = Snapshot<T>

type HandlePromise = <P extends Promise<any>>(promise: P) => Awaited<P>

type CreateSnapshot = <T extends object>(
target: T,
version: number,
handlePromise?: HandlePromise
) => T
type CreateSnapshot = <T extends object>(target: T, version: number) => T

type RemoveListener = () => void
type AddListener = (listener: Listener) => RemoveListener
Expand Down Expand Up @@ -86,29 +78,11 @@ const buildProxyFunction = (
!(x instanceof RegExp) &&
!(x instanceof ArrayBuffer),

defaultHandlePromise = <P extends Promise<any>>(
promise: P & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: Awaited<P>
reason?: unknown
}
) => {
switch (promise.status) {
case 'fulfilled':
return promise.value as Awaited<P>
case 'rejected':
throw promise.reason
default:
throw promise
}
},

snapCache = new WeakMap<object, [version: number, snap: unknown]>(),

createSnapshot: CreateSnapshot = <T extends object>(
target: T,
version: number,
handlePromise: HandlePromise = defaultHandlePromise
version: number
): T => {
const cache = snapCache.get(target)
if (cache?.[0] === version) {
Expand Down Expand Up @@ -138,18 +112,11 @@ const buildProxyFunction = (
}
if (refSet.has(value as object)) {
markToTrack(value as object, false) // mark not to track
} else if (value instanceof Promise) {
delete desc.value
desc.get = () => handlePromise(value)
} else if (proxyStateMap.has(value as object)) {
const [target, ensureVersion] = proxyStateMap.get(
value as object
) as ProxyState
desc.value = createSnapshot(
target,
ensureVersion(),
handlePromise
) as Snapshot<T>
desc.value = createSnapshot(target, ensureVersion()) as Snapshot<T>
}
Object.defineProperty(snap, key, desc)
})
Expand Down Expand Up @@ -337,7 +304,6 @@ const buildProxyFunction = (
objectIs,
newProxy,
canProxy,
defaultHandlePromise,
snapCache,
createSnapshot,
proxyCache,
Expand Down Expand Up @@ -391,16 +357,13 @@ export function subscribe<T extends object>(
}
}

export function snapshot<T extends object>(
proxyObject: T,
handlePromise?: HandlePromise
): Snapshot<T> {
export function snapshot<T extends object>(proxyObject: T): Snapshot<T> {
const proxyState = proxyStateMap.get(proxyObject as object)
if (import.meta.env?.MODE !== 'production' && !proxyState) {
console.warn('Please use proxy object')
}
const [target, ensureVersion, createSnapshot] = proxyState as ProxyState
return createSnapshot(target, ensureVersion(), handlePromise) as Snapshot<T>
return createSnapshot(target, ensureVersion()) as Snapshot<T>
}

export function ref<T extends object>(obj: T): T & AsRef {
Expand Down
100 changes: 54 additions & 46 deletions tests/async.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { StrictMode, Suspense } from 'react'
/// <reference types="react/canary" />

import ReactExports, { StrictMode, Suspense } from 'react'
import { fireEvent, render, waitFor } from '@testing-library/react'
import { it } from 'vitest'
import { proxy, useSnapshot } from 'valtio'
Expand All @@ -8,7 +10,10 @@ const sleep = (ms: number) =>
setTimeout(resolve, ms)
})

it('delayed increment', async () => {
const { use } = ReactExports as any // for TS < 4.3 FIXME later
const use2 = (x: any) => (x instanceof Promise ? use(x) : x)

it.skipIf(typeof use === 'undefined')('delayed increment', async () => {
const state = proxy<any>({ count: 0 })
const delayedIncrement = () => {
const nextCount = state.count + 1
Expand All @@ -19,7 +24,7 @@ it('delayed increment', async () => {
const snap = useSnapshot(state)
return (
<>
<div>count: {snap.count}</div>
<div>count: {use2(snap.count)}</div>
<button onClick={delayedIncrement}>button</button>
</>
)
Expand All @@ -40,7 +45,7 @@ it('delayed increment', async () => {
await findByText('count: 1')
})

it('delayed object', async () => {
it.skipIf(typeof use === 'undefined')('delayed object', async () => {
const state = proxy<any>({ object: { text: 'none' } })
const delayedObject = () => {
state.object = sleep(300).then(() => ({ text: 'hello' }))
Expand All @@ -50,7 +55,7 @@ it('delayed object', async () => {
const snap = useSnapshot(state)
return (
<>
<div>text: {snap.object.text}</div>
<div>text: {use2(snap.object).text}</div>
<button onClick={delayedObject}>button</button>
</>
)
Expand All @@ -71,51 +76,54 @@ it('delayed object', async () => {
await findByText('text: hello')
})

it('delayed object update fulfilled', async () => {
const state = proxy<any>({
object: sleep(300).then(() => ({ text: 'counter', count: 0 })),
})
const updateObject = () => {
state.object = state.object.then((v: any) =>
sleep(300).then(() => ({ ...v, count: v.count + 1 }))
it.skipIf(typeof use === 'undefined')(
'delayed object update fulfilled',
async () => {
const state = proxy<any>({
object: sleep(300).then(() => ({ text: 'counter', count: 0 })),
})
const updateObject = () => {
state.object = state.object.then((v: any) =>
sleep(300).then(() => ({ ...v, count: v.count + 1 }))
)
}

const Counter = () => {
const snap = useSnapshot(state)
return (
<>
<div>text: {use2(snap.object).text}</div>
<div>count: {use2(snap.object).count}</div>
<button onClick={updateObject}>button</button>
</>
)
}

const { getByText, findByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Counter />
</Suspense>
</StrictMode>
)
}

const Counter = () => {
const snap = useSnapshot(state)
return (
<>
<div>text: {snap.object.text}</div>
<div>count: {snap.object.count}</div>
<button onClick={updateObject}>button</button>
</>
)
}
await findByText('loading')
await waitFor(() => {
getByText('text: counter')
getByText('count: 0')
})

const { getByText, findByText } = render(
<StrictMode>
<Suspense fallback="loading">
<Counter />
</Suspense>
</StrictMode>
)

await findByText('loading')
await waitFor(() => {
getByText('text: counter')
getByText('count: 0')
})
fireEvent.click(getByText('button'))

fireEvent.click(getByText('button'))

await findByText('loading')
await waitFor(() => {
getByText('text: counter')
getByText('count: 1')
})
})
await findByText('loading')
await waitFor(() => {
getByText('text: counter')
getByText('count: 1')
})
}
)

it('delayed falsy value', async () => {
it.skipIf(typeof use === 'undefined')('delayed falsy value', async () => {
const state = proxy<any>({ value: true })
const delayedValue = () => {
state.value = sleep(300).then(() => null)
Expand All @@ -125,7 +133,7 @@ it('delayed falsy value', async () => {
const snap = useSnapshot(state)
return (
<>
<div>value: {String(snap.value)}</div>
<div>value: {String(use2(snap.value))}</div>
<button onClick={delayedValue}>button</button>
</>
)
Expand Down
11 changes: 8 additions & 3 deletions tests/computed.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { StrictMode, Suspense } from 'react'
/// <reference types="react/canary" />

import ReactExports, { StrictMode, Suspense } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { memoize } from 'proxy-memoize'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { addComputed, proxyWithComputed, subscribeKey } from 'valtio/utils'

const { use } = ReactExports as any // for TS < 4.3 FIXME later
const use2 = (x: any) => (x instanceof Promise ? use(x) : x)

const consoleWarn = console.warn
beforeEach(() => {
console.warn = vi.fn((message: string) => {
Expand Down Expand Up @@ -201,7 +206,7 @@ describe('DEPRECATED addComputed', () => {
expect(callback).toBeCalledTimes(2)
})

it('async addComputed', async () => {
it.skipIf(typeof use === 'undefined')('async addComputed', async () => {
const state = proxy({ count: 0 })
addComputed(state, {
delayedCount: async (snap) => {
Expand All @@ -217,7 +222,7 @@ describe('DEPRECATED addComputed', () => {
return (
<>
<div>
count: {snap.count}, delayedCount: {snap.delayedCount}
count: {snap.count}, delayedCount: {use2(snap.delayedCount)}
</div>
<button onClick={() => ++state.count}>button</button>
</>
Expand Down
11 changes: 8 additions & 3 deletions tests/derive.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { StrictMode, Suspense, useEffect, useRef } from 'react'
/// <reference types="react/canary" />

import ReactExports, { StrictMode, Suspense, useEffect, useRef } from 'react'
import { fireEvent, render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
Expand All @@ -11,6 +13,9 @@ const sleep = (ms: number) =>
setTimeout(resolve, ms)
})

const { use } = ReactExports as any // for TS < 4.3 FIXME later
const use2 = (x: any) => (x instanceof Promise ? use(x) : x)

it('basic derive', async () => {
const computeDouble = vi.fn((x: number) => x * 2)
const state = proxy({
Expand Down Expand Up @@ -149,7 +154,7 @@ it('derive with two dependencies', async () => {
expect(callback).toBeCalledTimes(2)
})

it('async derive', async () => {
it.skipIf(typeof use === 'undefined')('async derive', async () => {
const state = proxy({ count: 0 })
derive(
{
Expand All @@ -168,7 +173,7 @@ it('async derive', async () => {
return (
<>
<div>
count: {snap.count}, delayedCount: {snap.delayedCount}
count: {snap.count}, delayedCount: {use2(snap.delayedCount)}
</div>
<button onClick={() => ++state.count}>button</button>
</>
Expand Down
Loading
Loading