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.2.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.2.1
Choose a head ref
  • 8 commits
  • 12 files changed
  • 4 contributors

Commits on Jun 17, 2023

  1. 1

    Verified

    This commit was signed with the committer’s verified signature.
    kwasniew Mateusz Kwasniewski
    Copy the full SHA
    3040629 View commit details

Commits on Jun 18, 2023

  1. feat(utils/useHydrateAtoms) - Optionally rehydrate with force hydrate (

    …#1990)
    
    * Added forceHydrate option to useHydrateAtoms
    
    * Updated docs for forceHydrate useHydrateAtoms
    
    * Updated forceHydrate to dangerouslyForceHydrate
    
    * Update tests/react/utils/useHydrateAtoms.test.tsx
    
    ---------
    
    Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
    SariSabouh and dai-shi authored Jun 18, 2023
    1

    Verified

    This commit was signed with the committer’s verified signature.
    kwasniew Mateusz Kwasniewski
    Copy the full SHA
    358b873 View commit details
  2. fix(utils): revert atomWithStorage typing (#1994)

    * fix(utils): fix/revert atomWithStorage typing
    
    * refactor
    dai-shi authored Jun 18, 2023
    1

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    77feeb2 View commit details
  3. fix(utils): improve selectAtom typing (#1995)

    * add failing test
    
    * fix(utils): improve selectAtom typing
    
    * refactor
    dai-shi authored Jun 18, 2023
    1
    Copy the full SHA
    91a92e3 View commit details
  4. fix(utils): improve unstable_unwrap for sometimes async atom (#1996)

    * add failing test
    
    * fix(utils): improve unstable_unwrap for sometimes async atom
    
    * add a test
    
    * fix lint
    
    * refactor
    dai-shi authored Jun 18, 2023
    1
    Copy the full SHA
    39059bb View commit details

Commits on Jun 19, 2023

  1. fix(utils): unstable_unwrap to support writable atom (#1997)

    * fix(utils): unwrap to support writable atom
    
    * fix types
    
    * fix type test
    
    * fix type again
    dai-shi authored Jun 19, 2023
    1
    Copy the full SHA
    f7c60c1 View commit details
  2. 1
    Copy the full SHA
    cbb8d56 View commit details
  3. 2.2.1

    dai-shi committed Jun 19, 2023
    1
    Copy the full SHA
    2188d75 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-21a161fa3-20230609
- 0.0.0-experimental-21a161fa3-20230609
- 18.3.0-canary-613e6f5fc-20230616
- 0.0.0-experimental-613e6f5fc-20230616
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
18 changes: 14 additions & 4 deletions docs/tools/devtools.mdx
Original file line number Diff line number Diff line change
@@ -37,11 +37,9 @@ npm install jotai-devtools @emotion/react --save

Enhance your development experience with the UI based Jotai DevTool.

### Usage

#### Babel plugin setup - (_Optional but highly recommended_)

Use Jotai babel plugins for optimal debugging experience. Find the complete guide on the [babel](../tools/babel) page
Use Jotai babel plugins for optimal debugging experience. Find the complete guide on the [babel](../tools/babel) page and/or [swc](../tools/swc) page.

Eg.

@@ -85,14 +83,26 @@ type DevToolsProps = {
// Custom nonce to allowlist jotai-devtools specific inline styles via CSP
nonce?: string
options?: {
// Private atoms are used internally by atoms like `atomWithStorage`
// Private atoms are used internally in atom creators like `atomWithStorage`
// or `atomWithLocation`, etc. to manage the internal state.
// Defaults to `false`
shouldShowPrivateAtoms?: boolean
// Expands the JSON tree view on initial render on Atom Viewer tab, Timeline tab, etc.
// Defaults to `false`
shouldExpandJsonTreeViewInitially?: boolean
// The interval (in milliseconds) between each step of the time travel playback.
// Defaults to `750ms`
timeTravelPlaybackInterval?: number
// The maximum number of snapshots to keep in the history.
// The higher the number the more memory it will consume.
// Defaults to `Infinity`. Recommended: `~30`
snapshotHistoryLimit?: number
}
}
```
### Usage
### Provider-less
```tsx
14 changes: 14 additions & 0 deletions docs/utilities/ssr.mdx
Original file line number Diff line number Diff line change
@@ -48,6 +48,20 @@ useHydrateAtoms(new Map([[count, 42]]))
```

Atoms can only be hydrated once per store. Therefore, if the initial value used is changed during rerenders, it won't update the atom value.
If there is a unique need to re-hydrate a previously hydrated atom, pass the optional dangerouslyForceHydrate as true
and note that it may behave wrongly in concurrent rendering.

```js
useHydrateAtoms(
[
[countAtom, 42],
[frameworkAtom, 'Next.js'],
],
{
dangerouslyForceHydrate: true,
}
)
```

If there's a need to hydrate in multiple stores, use multiple `useHydrateAtoms` hooks to achieve that.

22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jotai",
"private": true,
"version": "2.2.0",
"version": "2.2.1",
"description": "👻 Primitive and flexible state management for React",
"main": "./index.js",
"types": "./index.d.ts",
@@ -126,21 +126,21 @@
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.1.1",
"@testing-library/dom": "^9.3.0",
"@testing-library/dom": "^9.3.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/babel__core": "^7.20.1",
"@types/react": "^18.2.11",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"@vitest/coverage-c8": "^0.32.0",
"@vitest/ui": "^0.32.0",
"@types/react": "^18.2.12",
"@types/react-dom": "^18.2.5",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"@vitest/coverage-c8": "^0.32.2",
"@vitest/ui": "^0.32.2",
"benny": "^3.7.1",
"concurrently": "^8.2.0",
"downlevel-dts": "^0.11.0",
"esbuild": "^0.18.1",
"eslint": "^8.42.0",
"esbuild": "^0.18.4",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.27.5",
@@ -162,7 +162,7 @@
"ts-node": "^10.9.1",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"vitest": "^0.32.0",
"vitest": "^0.32.2",
"wonka": "^6.3.2"
},
"peerDependencies": {
6 changes: 4 additions & 2 deletions src/react/utils/useHydrateAtoms.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,9 @@ import { useStore } from '../../react.ts'
import type { WritableAtom } from '../../vanilla.ts'

type Store = ReturnType<typeof useStore>
type Options = Parameters<typeof useStore>[0]
type Options = Parameters<typeof useStore>[0] & {
dangerouslyForceHydrate?: boolean
}
type AnyWritableAtom = WritableAtom<unknown, any[], any>
type AtomMap<A = AnyWritableAtom, V = unknown> = Map<A, V>
type AtomTuple<A = AnyWritableAtom, V = unknown> = readonly [A, V]
@@ -36,7 +38,7 @@ export function useHydrateAtoms<T extends Iterable<AtomTuple>>(

const hydratedSet = getHydratedSet(store)
for (const [atom, value] of values) {
if (!hydratedSet.has(atom)) {
if (!hydratedSet.has(atom) || options?.dangerouslyForceHydrate) {
hydratedSet.add(atom)
store.set(atom, value)
}
20 changes: 11 additions & 9 deletions src/vanilla/utils/atomWithStorage.ts
Original file line number Diff line number Diff line change
@@ -123,9 +123,9 @@ export function atomWithStorage<Value>(
storage: AsyncStorage<Value>,
unstable_options?: { unstable_getOnInit?: boolean }
): WritableAtom<
PromiseLike<Value> | Value,
[SetStateActionWithReset<PromiseLike<Value> | Value>],
PromiseLike<void>
Value | Promise<Value>,
[SetStateActionWithReset<Value | Promise<Value>>],
Promise<void>
>

export function atomWithStorage<Value>(
@@ -145,7 +145,9 @@ export function atomWithStorage<Value>(
): any {
const getOnInit = unstable_options?.unstable_getOnInit
const baseAtom = atom(
getOnInit ? storage.getItem(key, initialValue) : initialValue
getOnInit
? (storage.getItem(key, initialValue) as Value | Promise<Value>)
: initialValue
)

if (import.meta.env?.MODE !== 'production') {
@@ -154,7 +156,7 @@ export function atomWithStorage<Value>(

baseAtom.onMount = (setAtom) => {
if (!getOnInit) {
setAtom(storage.getItem(key, initialValue))
setAtom(storage.getItem(key, initialValue) as Value | Promise<Value>)
}
let unsub: Unsubscribe | undefined
if (storage.subscribe) {
@@ -165,20 +167,20 @@ export function atomWithStorage<Value>(

const anAtom = atom(
(get) => get(baseAtom),
(get, set, update: SetStateActionWithReset<PromiseLike<Value> | Value>) => {
(get, set, update: SetStateActionWithReset<Value | Promise<Value>>) => {
const nextValue =
typeof update === 'function'
? (
update as (
prev: PromiseLike<Value> | Value
) => PromiseLike<Value> | Value | typeof RESET
prev: Value | Promise<Value>
) => Value | Promise<Value> | typeof RESET
)(get(baseAtom))
: update
if (nextValue === RESET) {
set(baseAtom, initialValue)
return storage.removeItem(key)
}
if (isPromiseLike(nextValue)) {
if (nextValue instanceof Promise) {
return nextValue.then((resolvedValue) => {
set(baseAtom, resolvedValue)
return storage.setItem(key, resolvedValue)
8 changes: 1 addition & 7 deletions src/vanilla/utils/selectAtom.ts
Original file line number Diff line number Diff line change
@@ -15,17 +15,11 @@ const memo3 = <T>(
return getCached(create, cache3, dep3)
}

export function selectAtom<Value, Slice>(
anAtom: Atom<Promise<Value>>,
selector: (v: Awaited<Value>, prevSlice?: Slice) => Slice,
equalityFn?: (a: Slice, b: Slice) => boolean
): Atom<Promise<Slice>>

export function selectAtom<Value, Slice>(
anAtom: Atom<Value>,
selector: (v: Awaited<Value>, prevSlice?: Slice) => Slice,
equalityFn?: (a: Slice, b: Slice) => boolean
): Atom<Slice>
): Atom<Value extends Promise<unknown> ? Promise<Slice> : Slice>

export function selectAtom<Value, Slice>(
anAtom: Atom<Value>,
34 changes: 23 additions & 11 deletions src/vanilla/utils/unwrap.ts
Original file line number Diff line number Diff line change
@@ -11,27 +11,36 @@ const memo2 = <T>(create: () => T, dep1: object, dep2: object): T => {

const defaultFallback = () => undefined

export function unwrap<Value, Args extends unknown[], Result>(
anAtom: WritableAtom<Value, Args, Result>
): WritableAtom<Awaited<Value> | undefined, Args, Result>

export function unwrap<Value, Args extends unknown[], Result, PendingValue>(
anAtom: WritableAtom<Value, Args, Result>,
fallback: (prev?: Awaited<Value>) => PendingValue
): WritableAtom<Awaited<Value> | PendingValue, Args, Result>

export function unwrap<Value>(
anAtom: Atom<Promise<Value>>
anAtom: Atom<Value>
): Atom<Awaited<Value> | undefined>

export function unwrap<Value, PendingValue>(
anAtom: Atom<Promise<Value>>,
fallback: (prev?: Value) => PendingValue
anAtom: Atom<Value>,
fallback: (prev?: Awaited<Value>) => PendingValue
): Atom<Awaited<Value> | PendingValue>

export function unwrap<Value, PendingValue>(
anAtom: Atom<Promise<Value>>,
fallback: (prev?: Value) => PendingValue = defaultFallback as any
): Atom<Awaited<Value> | PendingValue> {
export function unwrap<Value, Args extends unknown[], Result, PendingValue>(
anAtom: WritableAtom<Value, Args, Result> | Atom<Value>,
fallback: (prev?: Awaited<Value>) => PendingValue = defaultFallback as any
) {
return memo2(
() => {
type PromiseAndValue = { readonly p: Promise<Value> } & (
type PromiseAndValue = { readonly p?: Promise<unknown> } & (
| { readonly v: Awaited<Value> }
| { readonly f: PendingValue }
)
const promiseErrorCache = new WeakMap<Promise<Value>, unknown>()
const promiseResultCache = new WeakMap<Promise<Value>, Awaited<Value>>()
const promiseErrorCache = new WeakMap<Promise<unknown>, unknown>()
const promiseResultCache = new WeakMap<Promise<unknown>, Awaited<Value>>()
const refreshAtom = atom(0)

if (import.meta.env?.MODE !== 'production') {
@@ -45,6 +54,9 @@ export function unwrap<Value, PendingValue>(
get(refreshAtom)
const prev = get(promiseAndValueAtom) as PromiseAndValue | undefined
const promise = get(anAtom)
if (!(promise instanceof Promise)) {
return { v: promise as Awaited<Value> }
}
if (promise === prev?.p) {
if (promiseErrorCache.has(promise)) {
throw promiseErrorCache.get(promise)
@@ -86,7 +98,7 @@ export function unwrap<Value, PendingValue>(
return state.v
}
return state.f
})
}, (anAtom as WritableAtom<Value, unknown[], unknown>).write)
},
anAtom,
fallback
75 changes: 75 additions & 0 deletions tests/react/utils/useHydrateAtoms.test.tsx
Original file line number Diff line number Diff line change
@@ -240,3 +240,78 @@ it('useHydrateAtoms should respect onMount', async () => {
await findByText('count: 42')
expect(onMountFn).toHaveBeenCalledTimes(1)
})

it('passing dangerouslyForceHydrate to useHydrateAtoms will re-hydrated atoms', async () => {
const countAtom = atom(0)
const statusAtom = atom('fulfilled')

const Counter = ({
initialCount,
initialStatus,
dangerouslyForceHydrate = false,
}: {
initialCount: number
initialStatus: string
dangerouslyForceHydrate?: boolean
}) => {
useHydrateAtoms(
[
[countAtom, initialCount],
[statusAtom, initialStatus],
],
{
dangerouslyForceHydrate,
}
)
const [countValue, setCount] = useAtom(countAtom)
const [statusValue, setStatus] = useAtom(statusAtom)

return (
<>
<div>count: {countValue}</div>
<button onClick={() => setCount((count) => count + 1)}>dispatch</button>
<div>status: {statusValue}</div>
<button
onClick={() =>
setStatus((status) =>
status === 'fulfilled' ? 'rejected' : 'fulfilled'
)
}>
update
</button>
</>
)
}
const { findByText, getByText, rerender } = render(
<StrictMode>
<Counter initialCount={42} initialStatus="rejected" />
</StrictMode>
)

await findByText('count: 42')
await findByText('status: rejected')
fireEvent.click(getByText('dispatch'))
fireEvent.click(getByText('update'))
await findByText('count: 43')
await findByText('status: fulfilled')

rerender(
<StrictMode>
<Counter initialCount={65} initialStatus="rejected" />
</StrictMode>
)
await findByText('count: 43')
await findByText('status: fulfilled')

rerender(
<StrictMode>
<Counter
initialCount={11}
initialStatus="rejected"
dangerouslyForceHydrate={true}
/>
</StrictMode>
)
await findByText('count: 11')
await findByText('status: rejected')
})
Loading