Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pmndrs/jotai
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.1.0
Choose a base ref
...
head repository: pmndrs/jotai
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.1.1
Choose a head ref
  • 10 commits
  • 12 files changed
  • 7 contributors

Commits on May 8, 2023

  1. 1

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    10001a7 View commit details

Commits on May 19, 2023

  1. docs: fix typos on "initialize atom on render" (#1945)

    Just some minor typos.
    blissdev authored May 19, 2023
    1
    Copy the full SHA
    1adaa66 View commit details
  2. 1
    Copy the full SHA
    4fa2dd0 View commit details

Commits on May 26, 2023

  1. 1
    Copy the full SHA
    7a5ce17 View commit details

Commits on May 31, 2023

  1. 1
    Copy the full SHA
    7a67b23 View commit details

Commits on Jun 3, 2023

  1. fix(vanilla): Stable promise (#1933)

    * failing test case for renotification on equal promise
    
    * fix unstable promise reference
    
    * revert store.ts
    
    * restore wrapped promise
    
    * fix typing
    
    * refactor
    
    ---------
    
    Co-authored-by: daishi <daishi@axlight.com>
    backbone87 and dai-shi authored Jun 3, 2023
    1
    Copy the full SHA
    06d4abf View commit details
  2. fix(vanilla): update atoms with tree structure dependencies (reggress…

    …ion from v1) (#1959)
    
    * add failing test
    
    * rewrite recomputeDependents
    
    * safer infinite loop detection with recursive call
    
    * refactor with two weak maps
    
    * add more test
    
    * fix to pass the test
    dai-shi authored Jun 3, 2023
    1
    Copy the full SHA
    1e774e2 View commit details
  3. fix: prefer PromiseLike where appropriate (#1967)

    * fix: prefer PromiseLike where appropriate
    
    * missing optional operator
    
    * more conversion
    
    * fix types
    dai-shi authored Jun 3, 2023
    1
    Copy the full SHA
    43e70c3 View commit details
  4. 1
    Copy the full SHA
    382a790 View commit details
  5. 2.1.1

    dai-shi committed Jun 3, 2023
    1
    Copy the full SHA
    df86b18 View commit details
4 changes: 2 additions & 2 deletions .github/workflows/test-multiple-versions.yml
Original file line number Diff line number Diff line change
@@ -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
20 changes: 0 additions & 20 deletions .vscode/launch.json

This file was deleted.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -39,7 +39,7 @@ If you would like to contribute by fixing an open issue or developing a new feat
2. Install dependencies by running `yarn` in the `website` folder We use [version 1](https://classic.yarnpkg.com/lang/en/docs/install) of yarn
3. Run `yarn dev` to start the dev server
4. Navigate to [`http://localhost:9000`](http://localhost:9000) to view the docs
5. Naivgate to the `docs` folder and make necessary changes to the docs
5. Navigate to the `docs` folder and make necessary changes to the docs
6. Add your changes to the docs and see them live reloaded in the browser
7. Follow step 4 and onwards from the [general](#general) guide above to bring it to the finish line

4 changes: 2 additions & 2 deletions docs/guides/initialize-atom-on-render.mdx
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ There are times when you need to create an reusable component which uses atoms.

These atoms' initial state are determined by the props passed to the component.

Below is a basic example of illustrating how you can use `Provider` and its prop, `initialValues`, to initialize state.
Below is a basic example illustrating how you can use `Provider` and its prop, `initialValues`, to initialize state.

### Basic Example

@@ -73,7 +73,7 @@ export const TextDisplay = ({ initialTextValue }) => (
)
```

Now, we can easily resue `TextDisplay` component with different initial text values despite them referencing the "same" atom.
Now, we can easily reuse `TextDisplay` component with different initial text values despite them referencing the "same" atom.

```jsx
export default function App() {
2 changes: 1 addition & 1 deletion docs/recipes/custom-useatom-hooks.mdx
Original file line number Diff line number Diff line change
@@ -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, [])
)
```

2 changes: 1 addition & 1 deletion docs/utilities/storage.mdx
Original file line number Diff line number Diff line change
@@ -138,7 +138,7 @@ const storedNumberAtom = atomWithStorage('my-number', 0, {
}
},
setItem(key, value) {
localStorage.setItem(JSON.stringify(value))
localStorage.setItem(key, JSON.stringify(value))
},
removeItem(key) {
localStorage.removeItem(key)
56 changes: 28 additions & 28 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",
@@ -76,7 +76,7 @@
"test": "vitest --ui --coverage",
"test:ci": "vitest",
"patch-d-ts": "node -e \"var {entries}=require('./rollup.config.js');require('shelljs').find('dist/**/*.d.ts').forEach(f=>{entries.forEach(({find,replacement})=>require('shelljs').sed('-i',new RegExp(' from \\''+find.source.slice(0,-1)+'\\';$'),' from \\''+replacement+'\\';',f));require('shelljs').sed('-i',/ from '(\\.[^']+)\\.ts';$/,' from \\'\\$1\\';',f)})\"",
"copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && downlevel-dts dist dist/ts3.8 --to=3.8 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined; this.jest=undefined;\"",
"copy": "shx cp -r dist/src/* dist/esm && shx cp -r dist/src/* dist && shx rm -rf dist/src && shx rm -rf dist/{src,tests} && downlevel-dts dist dist/ts3.8 --to=3.8 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined;\"",
"patch-ts3.8": "node -e \"require('shelljs').find('dist/ts3.8/**/*.d.ts').forEach(f=>require('fs').appendFileSync(f,'declare type Awaited<T> = T extends Promise<infer V> ? V : T;'))\"",
"patch-old-ts": "shx touch dist/ts_version_3.8_and_above_is_required.d.ts",
"patch-esm-ts": "node -e \"require('shelljs').find('dist/esm/**/*.d.ts').forEach(f=>{var f2=f.replace(/\\.ts$/,'.mts');require('fs').renameSync(f,f2);require('shelljs').sed('-i',/ from '(\\.[^']+)';$/,' from \\'\\$1.mjs\\';',f2);require('shelljs').sed('-i',/^declare module '(\\.[^']+)'/,'declare module \\'\\$1.mjs\\'',f2)})\"",
@@ -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": {
10 changes: 7 additions & 3 deletions src/react/useAtomValue.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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) => {
@@ -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.
@@ -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
@@ -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
}
@@ -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) => {
@@ -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)
}
@@ -276,6 +299,7 @@ export const createStore = () => {
}
}
})
promise.orig = valueOrPromise as PromiseLike<Awaited<Value>>
promise.status = 'pending'
registerCancelPromise(promise, (next) => {
if (next) {
@@ -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>(
19 changes: 11 additions & 8 deletions src/vanilla/utils/atomWithStorage.ts
Original file line number Diff line number Diff line change
@@ -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> =
@@ -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,
@@ -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 {
@@ -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)
@@ -129,7 +132,7 @@ export function atomWithStorage<Value>(

baseAtom.onMount = (setAtom) => {
const value = storage.getItem(key, initialValue)
if (value instanceof Promise) {
if (isPromiseLike(value)) {
value.then((v) => setAtom(v))
} else {
setAtom(value)
Loading