Skip to content

Commit

Permalink
Merge branch 'main' into breaking/utils/improve-atom-with-storage
Browse files Browse the repository at this point in the history
  • Loading branch information
dai-shi committed Jun 3, 2023
2 parents 0cb0995 + df86b18 commit f4adf6c
Show file tree
Hide file tree
Showing 8 changed files with 976 additions and 789 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test-multiple-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
- 18.0.0
- 18.1.0
- 18.2.0
- 18.3.0-canary-aef7ce554-20230503
- 0.0.0-experimental-aef7ce554-20230503
- 18.3.0-canary-e1ad4aa36-20230601
- 0.0.0-experimental-e1ad4aa36-20230601
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/custom-useatom-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function useSelectAtom(anAtom, keyFn) {
// how to use it
useSelectAtom(
useMemo(() => atom(initValue), [initValue]),
useCallBack((state) => state.prop, [])
useCallback((state) => state.prop, [])
)
```

Expand Down
54 changes: 27 additions & 27 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jotai",
"private": true,
"version": "2.1.0",
"version": "2.1.1",
"description": "👻 Primitive and flexible state management for React",
"main": "./index.js",
"types": "./index.d.ts",
Expand Down Expand Up @@ -113,56 +113,56 @@
},
"homepage": "https://github.com/pmndrs/jotai",
"devDependencies": {
"@babel/core": "^7.21.8",
"@babel/plugin-transform-react-jsx": "^7.21.5",
"@babel/plugin-transform-typescript": "^7.21.3",
"@babel/preset-env": "^7.21.5",
"@babel/template": "^7.20.7",
"@babel/types": "^7.21.5",
"@babel/core": "^7.22.1",
"@babel/plugin-transform-react-jsx": "^7.22.3",
"@babel/plugin-transform-typescript": "^7.22.3",
"@babel/preset-env": "^7.22.4",
"@babel/template": "^7.21.9",
"@babel/types": "^7.22.4",
"@redux-devtools/extension": "^3.2.5",
"@rollup/plugin-alias": "^5.0.0",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.1",
"@rollup/plugin-typescript": "^11.1.0",
"@testing-library/dom": "^9.2.0",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.1.1",
"@testing-library/dom": "^9.3.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/babel__core": "^7.20.0",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.3",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@vitest/coverage-c8": "^0.31.0",
"@vitest/ui": "^0.31.0",
"@types/babel__core": "^7.20.1",
"@types/react": "^18.2.8",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@vitest/coverage-c8": "^0.31.4",
"@vitest/ui": "^0.31.4",
"benny": "^3.7.1",
"concurrently": "^8.0.1",
"concurrently": "^8.1.0",
"downlevel-dts": "^0.11.0",
"esbuild": "^0.17.18",
"eslint": "^8.39.0",
"esbuild": "^0.17.19",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-vitest": "^0.1.5",
"jsdom": "^22.0.0",
"eslint-plugin-vitest": "^0.2.5",
"jsdom": "^22.1.0",
"json": "^11.0.0",
"prettier": "^2.8.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redux": "^4.2.1",
"rollup": "^3.21.4",
"rollup": "^3.23.0",
"rollup-plugin-esbuild": "^5.0.0",
"rxjs": "^7.8.1",
"shx": "^0.3.4",
"ts-expect": "^1.3.0",
"ts-node": "^10.9.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"vitest": "^0.31.0",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"vitest": "^0.31.4",
"wonka": "^6.3.2"
},
"peerDependencies": {
Expand Down
10 changes: 7 additions & 3 deletions src/react/useAtomValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { useStore } from './Provider.ts'

type Store = ReturnType<typeof useStore>

const isPromise = (x: unknown): x is Promise<unknown> => x instanceof Promise
const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
typeof (x as any)?.then === 'function'

const use =
ReactExports.use ||
(<T>(
promise: Promise<T> & {
promise: PromiseLike<T> & {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: unknown
Expand Down Expand Up @@ -99,5 +100,8 @@ export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
}, [store, atom, delay])

useDebugValue(value)
return isPromise(value) ? use(value) : (value as Awaited<Value>)
// TS doesn't allow using `use` always.
// The use of isPromiseLike is to be consistent with `use` type.
// `instanceof Promise` actually works fine in this case.
return isPromiseLike(value) ? use(value) : (value as Awaited<Value>)
}
80 changes: 68 additions & 12 deletions src/vanilla/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type PromiseMeta<T> = {
status?: 'pending' | 'fulfilled' | 'rejected'
value?: T
reason?: AnyError
orig?: PromiseLike<T>
}

const resolvePromise = <T>(promise: Promise<T> & PromiseMeta<T>, value: T) => {
Expand All @@ -54,6 +55,9 @@ const rejectPromise = <T>(
promise.reason = e
}

const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
typeof (x as any)?.then === 'function'

/**
* Immutable map from a dependency to the dependency's atom state
* when it was last read.
Expand Down Expand Up @@ -82,6 +86,11 @@ const hasPromiseAtomValue = <Value>(
): a is AtomState<Value> & { v: Value & Promise<unknown> } =>
'v' in a && a.v instanceof Promise

const isEqualPromiseAtomValue = <Value>(
a: AtomState<Promise<Value> & PromiseMeta<Value>>,
b: AtomState<Promise<Value> & PromiseMeta<Value>>
) => 'v' in a && 'v' in b && a.v.orig && a.v.orig === b.v.orig

const returnAtomValue = <Value>(atomState: AtomState<Value>): Value => {
if ('e' in atomState) {
throw atomState.e
Expand Down Expand Up @@ -214,6 +223,20 @@ export const createStore = () => {
// bail out
return prevAtomState
}
if (
prevAtomState &&
hasPromiseAtomValue(prevAtomState) &&
hasPromiseAtomValue(nextAtomState) &&
isEqualPromiseAtomValue(prevAtomState, nextAtomState)
) {
if (prevAtomState.d === nextAtomState.d) {
// bail out
return prevAtomState
} else {
// restore the wrapped promise
nextAtomState.v = prevAtomState.v
}
}
setAtomState(atom, nextAtomState)
return nextAtomState
}
Expand All @@ -224,7 +247,7 @@ export const createStore = () => {
nextDependencies?: NextDependencies,
abortPromise?: () => void
): AtomState<Value> => {
if (valueOrPromise instanceof Promise) {
if (isPromiseLike(valueOrPromise)) {
let continuePromise: (next: Promise<Awaited<Value>>) => void
const promise: Promise<Awaited<Value>> & PromiseMeta<Awaited<Value>> =
new Promise((resolve, reject) => {
Expand All @@ -241,7 +264,7 @@ export const createStore = () => {
nextDependencies
)
resolvePromise(promise, v)
resolve(v)
resolve(v as Awaited<Value>)
if (prevAtomState?.d !== nextAtomState.d) {
mountDependencies(atom, nextAtomState, prevAtomState?.d)
}
Expand Down Expand Up @@ -276,6 +299,7 @@ export const createStore = () => {
}
}
})
promise.orig = valueOrPromise as PromiseLike<Awaited<Value>>
promise.status = 'pending'
registerCancelPromise(promise, (next) => {
if (next) {
Expand Down Expand Up @@ -424,17 +448,49 @@ export const createStore = () => {
}
}

const recomputeDependents = <Value>(atom: Atom<Value>): void => {
const mounted = mountedMap.get(atom)
mounted?.t.forEach((dependent) => {
if (dependent !== atom) {
const prevAtomState = getAtomState(dependent)
const nextAtomState = readAtomState(dependent)
if (!prevAtomState || !isEqualAtomValue(prevAtomState, nextAtomState)) {
recomputeDependents(dependent)
const recomputeDependents = (atom: AnyAtom): void => {
const dependencyMap = new Map<AnyAtom, Set<AnyAtom>>()
const dirtyMap = new WeakMap<AnyAtom, number>()
const loop1 = (a: AnyAtom) => {
const mounted = mountedMap.get(a)
mounted?.t.forEach((dependent) => {
if (dependent !== a) {
dependencyMap.set(
dependent,
(dependencyMap.get(dependent) || new Set()).add(a)
)
dirtyMap.set(dependent, (dirtyMap.get(dependent) || 0) + 1)
loop1(dependent)
}
}
})
})
}
loop1(atom)
const loop2 = (a: AnyAtom) => {
const mounted = mountedMap.get(a)
mounted?.t.forEach((dependent) => {
if (dependent !== a) {
let dirtyCount = dirtyMap.get(dependent)
if (dirtyCount) {
dirtyMap.set(dependent, --dirtyCount)
}
if (!dirtyCount) {
let isChanged = !!dependencyMap.get(dependent)?.size
if (isChanged) {
const prevAtomState = getAtomState(dependent)
const nextAtomState = readAtomState(dependent)
isChanged =
!prevAtomState ||
!isEqualAtomValue(prevAtomState, nextAtomState)
}
if (!isChanged) {
dependencyMap.forEach((s) => s.delete(dependent))
}
}
loop2(dependent)
}
})
}
loop2(atom)
}

const writeAtomState = <Value, Args extends unknown[], Result>(
Expand Down
31 changes: 17 additions & 14 deletions src/vanilla/utils/atomWithStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { atom } from '../../vanilla.ts'
import type { WritableAtom } from '../../vanilla.ts'
import { RESET } from './constants.ts'

const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
typeof (x as any)?.then === 'function'

type Unsubscribe = () => void

type SetStateActionWithReset<Value> =
Expand All @@ -10,9 +13,9 @@ type SetStateActionWithReset<Value> =
| ((prev: Value) => Value | typeof RESET)

export interface AsyncStorage<Value> {
getItem: (key: string, initialValue: Value) => Promise<Value>
setItem: (key: string, newValue: Value) => Promise<void>
removeItem: (key: string) => Promise<void>
getItem: (key: string, initialValue: Value) => PromiseLike<Value>
setItem: (key: string, newValue: Value) => PromiseLike<void>
removeItem: (key: string) => PromiseLike<void>
subscribe?: (
key: string,
callback: (value: Value) => void,
Expand All @@ -32,9 +35,9 @@ export interface SyncStorage<Value> {
}

export interface AsyncStringStorage {
getItem: (key: string) => Promise<string | null>
setItem: (key: string, newValue: string) => Promise<void>
removeItem: (key: string) => Promise<void>
getItem: (key: string) => PromiseLike<string | null>
setItem: (key: string, newValue: string) => PromiseLike<void>
removeItem: (key: string) => PromiseLike<void>
}

export interface SyncStringStorage {
Expand Down Expand Up @@ -71,7 +74,7 @@ export function createJSONStorage<Value>(
return lastValue
}
const str = getStringStorage()?.getItem(key) ?? null
if (str instanceof Promise) {
if (isPromiseLike(str)) {
return str.then(parse)
}
return parse(str)
Expand Down Expand Up @@ -120,9 +123,9 @@ export function atomWithStorage<Value>(
storage: AsyncStorage<Value>,
unstable_options?: { unstable_getOnInit?: boolean }
): WritableAtom<
Promise<Value> | Value,
[SetStateActionWithReset<Promise<Value> | Value>],
Promise<void>
PromiseLike<Value> | Value,
[SetStateActionWithReset<PromiseLike<Value> | Value>],
PromiseLike<void>
>

export function atomWithStorage<Value>(
Expand Down Expand Up @@ -162,20 +165,20 @@ export function atomWithStorage<Value>(

const anAtom = atom(
(get) => get(baseAtom),
(get, set, update: SetStateActionWithReset<Promise<Value> | Value>) => {
(get, set, update: SetStateActionWithReset<PromiseLike<Value> | Value>) => {
const nextValue =
typeof update === 'function'
? (
update as (
prev: Promise<Value> | Value
) => Promise<Value> | Value | typeof RESET
prev: PromiseLike<Value> | Value
) => PromiseLike<Value> | Value | typeof RESET
)(get(baseAtom))
: update
if (nextValue === RESET) {
set(baseAtom, initialValue)
return storage.removeItem(key)
}
if (nextValue instanceof Promise) {
if (isPromiseLike(nextValue)) {
return nextValue.then((resolvedValue) => {
set(baseAtom, resolvedValue)
return storage.setItem(key, resolvedValue)
Expand Down
Loading

0 comments on commit f4adf6c

Please sign in to comment.