From 0380fe00a00494cae2ad33d2b99c04a91a0daed5 Mon Sep 17 00:00:00 2001 From: Thisen Date: Tue, 3 Aug 2021 21:06:11 +0200 Subject: [PATCH 01/17] wip --- src/core/Provider.ts | 2 +- src/core/contexts.ts | 22 +++++++++++----------- src/devtools/useAtomsSnapshot.ts | 2 +- src/devtools/useGotoAtomsSnapshot.ts | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/Provider.ts b/src/core/Provider.ts index d708f16522..6ab602438e 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -80,7 +80,7 @@ export const subscribeDebugStore = ( // We keep a reference to the atoms in Provider's registeredAtoms in dev mode, // so atoms aren't garbage collected by the WeakMap of mounted atoms const useDebugState = (store: StoreForDevelopment) => { - const debugMutableSource = store[3] + const debugMutableSource = store[4] const [state, atoms]: [State, Atom[]] = useMutableSource( debugMutableSource, getDebugStateAndAtoms, diff --git a/src/core/contexts.ts b/src/core/contexts.ts index 3a970c21fc..b091ca0422 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -15,22 +15,20 @@ type UpdateAtom = ( type CommitCallback = () => void type StoreForProduction = [ - stateMutableSource: MutableSource, - updateAtom: UpdateAtom, - commitCallback: CommitCallback + MutableSource, // stateMutableSource + UpdateAtom, // updateAtom + CommitCallback, // commitCallback + (values: Iterable, unknown]>) => void // restore ] export type StoreForDevelopment = [ - stateMutableSource: MutableSource, - updateAtom: UpdateAtom, - commitCallback: CommitCallback, - debugMutableSource: MutableSource<{ + ...StoreForProduction, + MutableSource<{ version: number atoms: Atom[] state: State listeners: Set<() => void> - }>, - restore: (values: Iterable, unknown]>) => void + }> // debugMutableSource ] export type Store = StoreForProduction | StoreForDevelopment @@ -45,7 +43,9 @@ const createStoreForProduction = ( atom: WritableAtom, update: Update ) => writeAtom(state, atom, update) - return [stateMutableSource, updateAtom, commitCallback] + const restore = (values: Iterable, unknown]>) => + restoreAtoms(state, values) + return [stateMutableSource, updateAtom, commitCallback, restore] } const createStoreForDevelopment = ( @@ -85,8 +85,8 @@ const createStoreForDevelopment = ( stateMutableSource, updateAtom, commitCallback, - debugMutableSource, restore, + debugMutableSource, ] } diff --git a/src/devtools/useAtomsSnapshot.ts b/src/devtools/useAtomsSnapshot.ts index 37d15a9637..0018f4d58f 100644 --- a/src/devtools/useAtomsSnapshot.ts +++ b/src/devtools/useAtomsSnapshot.ts @@ -12,7 +12,7 @@ type AtomsSnapshot = Map, unknown> export function useAtomsSnapshot(scope?: Scope): AtomsSnapshot { const StoreContext = getStoreContext(scope) - const debugMutableSource = useContext(StoreContext)[3] + const debugMutableSource = useContext(StoreContext)[4] if (debugMutableSource === undefined) { throw Error('useAtomsSnapshot can only be used in dev mode.') diff --git a/src/devtools/useGotoAtomsSnapshot.ts b/src/devtools/useGotoAtomsSnapshot.ts index 6d0d1a841a..c8cffa88b0 100644 --- a/src/devtools/useGotoAtomsSnapshot.ts +++ b/src/devtools/useGotoAtomsSnapshot.ts @@ -11,7 +11,7 @@ export function useGotoAtomsSnapshot(scope?: Scope) { if (!isDevStore(store)) { throw new Error('useGotoAtomsSnapshot can only be used in dev mode.') } - const restore = store[4] + const restore = store[3] return useCallback( (values: Parameters[0]) => { for (const [atom] of values) { From 98f9b1d4140a0000cda5d8914edb351f6715dd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 3 Aug 2021 21:41:11 +0200 Subject: [PATCH 02/17] useHydrateAtoms wip --- .size-snapshot.json | 28 ++++++++++++++-------------- src/utils/useHydrateAtoms.ts | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 src/utils/useHydrateAtoms.ts diff --git a/.size-snapshot.json b/.size-snapshot.json index cb4e7699c7..2250ea9406 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 21268, - "minified": 10175, - "gzipped": 3361, + "bundled": 21336, + "minified": 10196, + "gzipped": 3358, "treeshaked": { "rollup": { "code": 14, @@ -14,9 +14,9 @@ } }, "utils.js": { - "bundled": 15954, - "minified": 7668, - "gzipped": 2832, + "bundled": 16025, + "minified": 7685, + "gzipped": 2838, "treeshaked": { "rollup": { "code": 28, @@ -28,8 +28,8 @@ } }, "devtools.js": { - "bundled": 19122, - "minified": 9544, + "bundled": 19190, + "minified": 9565, "gzipped": 3281, "treeshaked": { "rollup": { @@ -42,9 +42,9 @@ } }, "immer.js": { - "bundled": 1566, - "minified": 839, - "gzipped": 402, + "bundled": 1637, + "minified": 856, + "gzipped": 418, "treeshaked": { "rollup": { "code": 42, @@ -56,9 +56,9 @@ } }, "optics.js": { - "bundled": 1613, - "minified": 811, - "gzipped": 425, + "bundled": 1684, + "minified": 828, + "gzipped": 440, "treeshaked": { "rollup": { "code": 32, diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts new file mode 100644 index 0000000000..9da10a9f56 --- /dev/null +++ b/src/utils/useHydrateAtoms.ts @@ -0,0 +1,17 @@ +import { useContext, useRef } from 'react' +import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' +import type { Atom, Scope } from '../core/atom' + +export function useHydrateAtoms( + values: Iterable, unknown]>, + scope?: Scope +) { + const hasRestoredRef = useRef(false) + const StoreContext = getStoreContext(scope) + const restoreAtoms = useContext(StoreContext)[3] + + if (!hasRestoredRef.current) { + hasRestoredRef.current = true + restoreAtoms(values) + } +} From 41b33242f23c8b9b3aff5611c3005339b6599260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Wed, 4 Aug 2021 21:02:17 +0200 Subject: [PATCH 03/17] Add first test --- src/utils.ts | 1 + tests/utils/useHydrateAtoms.test.tsx | 39 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/utils/useHydrateAtoms.test.tsx diff --git a/src/utils.ts b/src/utils.ts index 07d8b78b2f..c0488144d7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -18,3 +18,4 @@ export { createJSONStorage, } from './utils/atomWithStorage' export { atomWithObservable } from './utils/atomWithObservable' +export { useHydrateAtoms } from './utils/useHydrateAtoms' diff --git a/tests/utils/useHydrateAtoms.test.tsx b/tests/utils/useHydrateAtoms.test.tsx new file mode 100644 index 0000000000..0d34082218 --- /dev/null +++ b/tests/utils/useHydrateAtoms.test.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react' +import { fireEvent, render } from '@testing-library/react' +import { atom, useAtom } from '../../src/index' +import { useHydrateAtoms } from '../../src/utils' +import { getTestProvider } from '../testUtils' + +const Provider = getTestProvider() + +it('useHydrateAtoms should only hydrate on first render', async () => { + const countAtom = atom(0) + + const Counter: FC<{ count: number }> = ({ count }) => { + useHydrateAtoms([[countAtom, count]]) + const [countValue, setCount] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + + ) + } + const { findByText, getByText, rerender } = render( + + + + ) + + await findByText('count: 42') + fireEvent.click(getByText('dispatch')) + await findByText('count: 43') + + rerender( + + + + ) + await findByText('count: 43') +}) From 797619cf48e6c6ef1c18d7edbd547e3da239323c Mon Sep 17 00:00:00 2001 From: Thisen Date: Thu, 5 Aug 2021 21:26:32 +0200 Subject: [PATCH 04/17] More test cases --- tests/utils/useHydrateAtoms.test.tsx | 62 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/utils/useHydrateAtoms.test.tsx b/tests/utils/useHydrateAtoms.test.tsx index 0d34082218..adb6d20630 100644 --- a/tests/utils/useHydrateAtoms.test.tsx +++ b/tests/utils/useHydrateAtoms.test.tsx @@ -1,4 +1,4 @@ -import type { FC } from 'react' +import { FC, useRef } from 'react' import { fireEvent, render } from '@testing-library/react' import { atom, useAtom } from '../../src/index' import { useHydrateAtoms } from '../../src/utils' @@ -37,3 +37,63 @@ it('useHydrateAtoms should only hydrate on first render', async () => { ) await findByText('count: 43') }) + +it('useHydrateAtoms should not trigger unnessesary rerenders', async () => { + const countAtom = atom(0) + + const Counter: FC<{ count: number }> = ({ count }) => { + useHydrateAtoms([[countAtom, count]]) + const [countValue, setCount] = useAtom(countAtom) + const renderCount = useRef(0) + ++renderCount.current + return ( + <> +
renders: {renderCount.current}
+
count: {countValue}
+ + + ) + } + + const { findByText, getByText } = render( + + + + ) + + await findByText('count: 42') + await findByText('renders: 1') + fireEvent.click(getByText('dispatch')) + await findByText('count: 43') + await findByText('renders: 2') +}) + +it('useHydrateAtoms should work with derived atoms', async () => { + const countAtom = atom(0) + const doubleAtom = atom((get) => get(countAtom) * 2) + + const Counter: FC<{ count: number }> = ({ count }) => { + useHydrateAtoms([[countAtom, count]]) + const [countValue, setCount] = useAtom(countAtom) + const [doubleCount] = useAtom(doubleAtom) + return ( + <> +
count: {countValue}
+
doubleCount: {doubleCount}
+ + + ) + } + + const { findByText, getByText } = render( + + + + ) + + await findByText('count: 42') + await findByText('doubleCount: 84') + fireEvent.click(getByText('dispatch')) + await findByText('count: 43') + await findByText('doubleCount: 86') +}) From 962d8f58eb95dda5b6fe3be0b64a6abe68e1aa34 Mon Sep 17 00:00:00 2001 From: Thisen Date: Sat, 7 Aug 2021 10:42:21 +0200 Subject: [PATCH 05/17] Simplify implementation --- src/core/Provider.ts | 2 +- src/core/contexts.ts | 22 +++++++++++----------- src/devtools/useAtomsSnapshot.ts | 2 +- src/devtools/useGotoAtomsSnapshot.ts | 2 +- src/utils/useHydrateAtoms.ts | 16 ++++++++-------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/core/Provider.ts b/src/core/Provider.ts index 6ab602438e..d708f16522 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -80,7 +80,7 @@ export const subscribeDebugStore = ( // We keep a reference to the atoms in Provider's registeredAtoms in dev mode, // so atoms aren't garbage collected by the WeakMap of mounted atoms const useDebugState = (store: StoreForDevelopment) => { - const debugMutableSource = store[4] + const debugMutableSource = store[3] const [state, atoms]: [State, Atom[]] = useMutableSource( debugMutableSource, getDebugStateAndAtoms, diff --git a/src/core/contexts.ts b/src/core/contexts.ts index b091ca0422..3a970c21fc 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -15,20 +15,22 @@ type UpdateAtom = ( type CommitCallback = () => void type StoreForProduction = [ - MutableSource, // stateMutableSource - UpdateAtom, // updateAtom - CommitCallback, // commitCallback - (values: Iterable, unknown]>) => void // restore + stateMutableSource: MutableSource, + updateAtom: UpdateAtom, + commitCallback: CommitCallback ] export type StoreForDevelopment = [ - ...StoreForProduction, - MutableSource<{ + stateMutableSource: MutableSource, + updateAtom: UpdateAtom, + commitCallback: CommitCallback, + debugMutableSource: MutableSource<{ version: number atoms: Atom[] state: State listeners: Set<() => void> - }> // debugMutableSource + }>, + restore: (values: Iterable, unknown]>) => void ] export type Store = StoreForProduction | StoreForDevelopment @@ -43,9 +45,7 @@ const createStoreForProduction = ( atom: WritableAtom, update: Update ) => writeAtom(state, atom, update) - const restore = (values: Iterable, unknown]>) => - restoreAtoms(state, values) - return [stateMutableSource, updateAtom, commitCallback, restore] + return [stateMutableSource, updateAtom, commitCallback] } const createStoreForDevelopment = ( @@ -85,8 +85,8 @@ const createStoreForDevelopment = ( stateMutableSource, updateAtom, commitCallback, - restore, debugMutableSource, + restore, ] } diff --git a/src/devtools/useAtomsSnapshot.ts b/src/devtools/useAtomsSnapshot.ts index 0018f4d58f..37d15a9637 100644 --- a/src/devtools/useAtomsSnapshot.ts +++ b/src/devtools/useAtomsSnapshot.ts @@ -12,7 +12,7 @@ type AtomsSnapshot = Map, unknown> export function useAtomsSnapshot(scope?: Scope): AtomsSnapshot { const StoreContext = getStoreContext(scope) - const debugMutableSource = useContext(StoreContext)[4] + const debugMutableSource = useContext(StoreContext)[3] if (debugMutableSource === undefined) { throw Error('useAtomsSnapshot can only be used in dev mode.') diff --git a/src/devtools/useGotoAtomsSnapshot.ts b/src/devtools/useGotoAtomsSnapshot.ts index c8cffa88b0..6d0d1a841a 100644 --- a/src/devtools/useGotoAtomsSnapshot.ts +++ b/src/devtools/useGotoAtomsSnapshot.ts @@ -11,7 +11,7 @@ export function useGotoAtomsSnapshot(scope?: Scope) { if (!isDevStore(store)) { throw new Error('useGotoAtomsSnapshot can only be used in dev mode.') } - const restore = store[3] + const restore = store[4] return useCallback( (values: Parameters[0]) => { for (const [atom] of values) { diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 9da10a9f56..5cf2977efb 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -1,17 +1,17 @@ -import { useContext, useRef } from 'react' -import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' -import type { Atom, Scope } from '../core/atom' +import { useRef } from 'react' +import type { Atom } from '../core/atom' export function useHydrateAtoms( - values: Iterable, unknown]>, - scope?: Scope + values: Iterable & { init?: unknown }, unknown]> ) { const hasRestoredRef = useRef(false) - const StoreContext = getStoreContext(scope) - const restoreAtoms = useContext(StoreContext)[3] if (!hasRestoredRef.current) { hasRestoredRef.current = true - restoreAtoms(values) + for (const [atom, value] of values) { + if ('init' in atom) { + atom.init = value + } + } } } From fd97d7ad8247e052de4c7695d7710fe6f748e9e4 Mon Sep 17 00:00:00 2001 From: Thisen Date: Sat, 7 Aug 2021 10:48:39 +0200 Subject: [PATCH 06/17] Update size-snapshot --- .size-snapshot.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 2250ea9406..64a60e7236 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 21336, - "minified": 10196, - "gzipped": 3358, + "bundled": 21268, + "minified": 10175, + "gzipped": 3361, "treeshaked": { "rollup": { "code": 14, @@ -14,9 +14,9 @@ } }, "utils.js": { - "bundled": 16025, - "minified": 7685, - "gzipped": 2838, + "bundled": 16311, + "minified": 7826, + "gzipped": 2897, "treeshaked": { "rollup": { "code": 28, @@ -28,8 +28,8 @@ } }, "devtools.js": { - "bundled": 19190, - "minified": 9565, + "bundled": 19122, + "minified": 9544, "gzipped": 3281, "treeshaked": { "rollup": { From 80d777c1642d38d26cd56cd03347fd6d137ebb0e Mon Sep 17 00:00:00 2001 From: Thisen Date: Mon, 9 Aug 2021 17:20:58 +0200 Subject: [PATCH 07/17] Revert to original implementation --- src/core/Provider.ts | 2 +- src/core/contexts.ts | 13 ++++++++----- src/devtools/useAtomsSnapshot.ts | 2 +- src/devtools/useGotoAtomsSnapshot.ts | 2 +- src/utils/useHydrateAtoms.ts | 16 ++++++++-------- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/core/Provider.ts b/src/core/Provider.ts index d708f16522..6ab602438e 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -80,7 +80,7 @@ export const subscribeDebugStore = ( // We keep a reference to the atoms in Provider's registeredAtoms in dev mode, // so atoms aren't garbage collected by the WeakMap of mounted atoms const useDebugState = (store: StoreForDevelopment) => { - const debugMutableSource = store[3] + const debugMutableSource = store[4] const [state, atoms]: [State, Atom[]] = useMutableSource( debugMutableSource, getDebugStateAndAtoms, diff --git a/src/core/contexts.ts b/src/core/contexts.ts index 3a970c21fc..ae0f9710ff 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -17,20 +17,21 @@ type CommitCallback = () => void type StoreForProduction = [ stateMutableSource: MutableSource, updateAtom: UpdateAtom, - commitCallback: CommitCallback + commitCallback: CommitCallback, + restore: (values: Iterable, unknown]>) => void ] export type StoreForDevelopment = [ stateMutableSource: MutableSource, updateAtom: UpdateAtom, commitCallback: CommitCallback, + restore: (values: Iterable, unknown]>) => void, debugMutableSource: MutableSource<{ version: number atoms: Atom[] state: State listeners: Set<() => void> - }>, - restore: (values: Iterable, unknown]>) => void + }> ] export type Store = StoreForProduction | StoreForDevelopment @@ -45,7 +46,9 @@ const createStoreForProduction = ( atom: WritableAtom, update: Update ) => writeAtom(state, atom, update) - return [stateMutableSource, updateAtom, commitCallback] + const restore = (values: Iterable, unknown]>) => + restoreAtoms(state, values) + return [stateMutableSource, updateAtom, commitCallback, restore] } const createStoreForDevelopment = ( @@ -85,8 +88,8 @@ const createStoreForDevelopment = ( stateMutableSource, updateAtom, commitCallback, - debugMutableSource, restore, + debugMutableSource, ] } diff --git a/src/devtools/useAtomsSnapshot.ts b/src/devtools/useAtomsSnapshot.ts index 37d15a9637..0018f4d58f 100644 --- a/src/devtools/useAtomsSnapshot.ts +++ b/src/devtools/useAtomsSnapshot.ts @@ -12,7 +12,7 @@ type AtomsSnapshot = Map, unknown> export function useAtomsSnapshot(scope?: Scope): AtomsSnapshot { const StoreContext = getStoreContext(scope) - const debugMutableSource = useContext(StoreContext)[3] + const debugMutableSource = useContext(StoreContext)[4] if (debugMutableSource === undefined) { throw Error('useAtomsSnapshot can only be used in dev mode.') diff --git a/src/devtools/useGotoAtomsSnapshot.ts b/src/devtools/useGotoAtomsSnapshot.ts index 6d0d1a841a..c8cffa88b0 100644 --- a/src/devtools/useGotoAtomsSnapshot.ts +++ b/src/devtools/useGotoAtomsSnapshot.ts @@ -11,7 +11,7 @@ export function useGotoAtomsSnapshot(scope?: Scope) { if (!isDevStore(store)) { throw new Error('useGotoAtomsSnapshot can only be used in dev mode.') } - const restore = store[4] + const restore = store[3] return useCallback( (values: Parameters[0]) => { for (const [atom] of values) { diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 5cf2977efb..9da10a9f56 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -1,17 +1,17 @@ -import { useRef } from 'react' -import type { Atom } from '../core/atom' +import { useContext, useRef } from 'react' +import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' +import type { Atom, Scope } from '../core/atom' export function useHydrateAtoms( - values: Iterable & { init?: unknown }, unknown]> + values: Iterable, unknown]>, + scope?: Scope ) { const hasRestoredRef = useRef(false) + const StoreContext = getStoreContext(scope) + const restoreAtoms = useContext(StoreContext)[3] if (!hasRestoredRef.current) { hasRestoredRef.current = true - for (const [atom, value] of values) { - if ('init' in atom) { - atom.init = value - } - } + restoreAtoms(values) } } From 65da656ac34799f6e08e765678bcb773b405a7f4 Mon Sep 17 00:00:00 2001 From: Thisen Date: Mon, 9 Aug 2021 20:46:46 +0200 Subject: [PATCH 08/17] Optimize hook and add tests --- src/utils/useHydrateAtoms.ts | 20 ++-- tests/utils/useHydrateAtoms.test.tsx | 143 ++++++++++++++++++++++++--- 2 files changed, 144 insertions(+), 19 deletions(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 9da10a9f56..f7e38aa535 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -1,4 +1,4 @@ -import { useContext, useRef } from 'react' +import { useContext, useMemo } from 'react' import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' import type { Atom, Scope } from '../core/atom' @@ -6,12 +6,20 @@ export function useHydrateAtoms( values: Iterable, unknown]>, scope?: Scope ) { - const hasRestoredRef = useRef(false) const StoreContext = getStoreContext(scope) const restoreAtoms = useContext(StoreContext)[3] - if (!hasRestoredRef.current) { - hasRestoredRef.current = true - restoreAtoms(values) - } + useMemo(() => { + const tuplesToRestore = [] + for (const tuple of values) { + const atom = tuple[0] + if ((atom as any).hydrated !== hydratedSymbol) { + tuplesToRestore.push(tuple) + ;(atom as any).hydrated = hydratedSymbol + } + } + restoreAtoms(tuplesToRestore) + }, [values, restoreAtoms]) } + +const hydratedSymbol = Symbol() diff --git a/tests/utils/useHydrateAtoms.test.tsx b/tests/utils/useHydrateAtoms.test.tsx index adb6d20630..d152e07896 100644 --- a/tests/utils/useHydrateAtoms.test.tsx +++ b/tests/utils/useHydrateAtoms.test.tsx @@ -9,20 +9,20 @@ const Provider = getTestProvider() it('useHydrateAtoms should only hydrate on first render', async () => { const countAtom = atom(0) - const Counter: FC<{ count: number }> = ({ count }) => { - useHydrateAtoms([[countAtom, count]]) + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) return ( <>
count: {countValue}
- + ) } const { findByText, getByText, rerender } = render( - + ) @@ -32,7 +32,7 @@ it('useHydrateAtoms should only hydrate on first render', async () => { rerender( - + ) await findByText('count: 43') @@ -41,8 +41,8 @@ it('useHydrateAtoms should only hydrate on first render', async () => { it('useHydrateAtoms should not trigger unnessesary rerenders', async () => { const countAtom = atom(0) - const Counter: FC<{ count: number }> = ({ count }) => { - useHydrateAtoms([[countAtom, count]]) + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) const renderCount = useRef(0) ++renderCount.current @@ -50,14 +50,14 @@ it('useHydrateAtoms should not trigger unnessesary rerenders', async () => { <>
renders: {renderCount.current}
count: {countValue}
- + ) } const { findByText, getByText } = render( - + ) @@ -72,22 +72,22 @@ it('useHydrateAtoms should work with derived atoms', async () => { const countAtom = atom(0) const doubleAtom = atom((get) => get(countAtom) * 2) - const Counter: FC<{ count: number }> = ({ count }) => { - useHydrateAtoms([[countAtom, count]]) + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) const [countValue, setCount] = useAtom(countAtom) const [doubleCount] = useAtom(doubleAtom) return ( <>
count: {countValue}
doubleCount: {doubleCount}
- + ) } const { findByText, getByText } = render( - + ) @@ -97,3 +97,120 @@ it('useHydrateAtoms should work with derived atoms', async () => { await findByText('count: 43') await findByText('doubleCount: 86') }) + +it('useHydrateAtoms can only restore an atom once', async () => { + const countAtom = atom(0) + + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) + const [countValue, setCount] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + + ) + } + const Counter2: FC<{ count: number }> = ({ count }) => { + useHydrateAtoms([[countAtom, count]]) + const [countValue, setCount] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + + ) + } + const { findByText, getByText, rerender } = render( + + + + ) + + await findByText('count: 42') + fireEvent.click(getByText('dispatch')) + await findByText('count: 43') + + rerender( + + + + ) + + await findByText('count: 43') + fireEvent.click(getByText('dispatch')) + await findByText('count: 44') +}) + +it('useHydrateAtoms can only restore an atom once', async () => { + const countAtom = atom(0) + + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) + const [countValue, setCount] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + + ) + } + const Counter2: FC<{ count: number }> = ({ count }) => { + useHydrateAtoms([[countAtom, count]]) + const [countValue, setCount] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + + ) + } + const { findByText, getByText, rerender } = render( + + + + ) + + await findByText('count: 42') + fireEvent.click(getByText('dispatch')) + await findByText('count: 43') + + rerender( + + + + ) + + await findByText('count: 43') + fireEvent.click(getByText('dispatch')) + await findByText('count: 44') +}) + +it('useHydrateAtoms should respect onMount', async () => { + const countAtom = atom(0) + const onMountFn = jest.fn() + countAtom.onMount = onMountFn + + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) + const [countValue] = useAtom(countAtom) + + return ( + <> +
count: {countValue}
+ + ) + } + const { findByText } = render( + + + + ) + + await findByText('count: 42') + expect(onMountFn).toBeCalledTimes(1) +}) From 46e04855ae87a3c2e2c3ecc8d4c7f5bccf276da0 Mon Sep 17 00:00:00 2001 From: Thisen Date: Mon, 9 Aug 2021 20:55:01 +0200 Subject: [PATCH 09/17] Fix types --- src/utils/useHydrateAtoms.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index f7e38aa535..5e9ff39250 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -3,7 +3,7 @@ import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' import type { Atom, Scope } from '../core/atom' export function useHydrateAtoms( - values: Iterable, unknown]>, + values: Iterable & { hydrated: Symbol }, unknown]>, scope?: Scope ) { const StoreContext = getStoreContext(scope) @@ -13,9 +13,9 @@ export function useHydrateAtoms( const tuplesToRestore = [] for (const tuple of values) { const atom = tuple[0] - if ((atom as any).hydrated !== hydratedSymbol) { + if (atom.hydrated !== hydratedSymbol) { tuplesToRestore.push(tuple) - ;(atom as any).hydrated = hydratedSymbol + atom.hydrated = hydratedSymbol } } restoreAtoms(tuplesToRestore) From 916c108f0ea6642d8a98e8b59a53cd55de053df7 Mon Sep 17 00:00:00 2001 From: Thisen Date: Mon, 9 Aug 2021 21:04:35 +0200 Subject: [PATCH 10/17] Fix types --- src/utils/useHydrateAtoms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 5e9ff39250..5f8c5ec0e1 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -3,7 +3,7 @@ import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' import type { Atom, Scope } from '../core/atom' export function useHydrateAtoms( - values: Iterable & { hydrated: Symbol }, unknown]>, + values: Iterable & { hydrated?: Symbol }, unknown]>, scope?: Scope ) { const StoreContext = getStoreContext(scope) From 89c6db3a7dfd86f4a248fea781b442a5b5bd10d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 09:00:31 +0200 Subject: [PATCH 11/17] Use symbol property for hydrated check --- src/utils/useHydrateAtoms.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 5f8c5ec0e1..a71efd92d5 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -2,8 +2,12 @@ import { useContext, useMemo } from 'react' import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' import type { Atom, Scope } from '../core/atom' +const hydratedSymbol = Symbol() + export function useHydrateAtoms( - values: Iterable & { hydrated?: Symbol }, unknown]>, + values: Iterable< + readonly [Atom & { [hydratedSymbol]?: boolean }, unknown] + >, scope?: Scope ) { const StoreContext = getStoreContext(scope) @@ -13,13 +17,11 @@ export function useHydrateAtoms( const tuplesToRestore = [] for (const tuple of values) { const atom = tuple[0] - if (atom.hydrated !== hydratedSymbol) { + if (atom[hydratedSymbol] !== true) { tuplesToRestore.push(tuple) - atom.hydrated = hydratedSymbol + atom[hydratedSymbol] = true } } restoreAtoms(tuplesToRestore) }, [values, restoreAtoms]) } - -const hydratedSymbol = Symbol() From 1ebe3a48ac3884167b068554a322c620c6720f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 10:17:47 +0200 Subject: [PATCH 12/17] Allow hydration on atom once per store --- src/utils/useHydrateAtoms.ts | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index a71efd92d5..0dd8c5a1cd 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -1,27 +1,37 @@ -import { useContext, useMemo } from 'react' +import { useContext } from 'react' import { SECRET_INTERNAL_getStoreContext as getStoreContext } from 'jotai' import type { Atom, Scope } from '../core/atom' +import type { Store } from '../core/contexts' -const hydratedSymbol = Symbol() +const hydratedMap: WeakMap>> = new WeakMap() export function useHydrateAtoms( - values: Iterable< - readonly [Atom & { [hydratedSymbol]?: boolean }, unknown] - >, + values: Iterable, unknown]>, scope?: Scope ) { const StoreContext = getStoreContext(scope) - const restoreAtoms = useContext(StoreContext)[3] + const store = useContext(StoreContext) + const restoreAtoms = store[3] - useMemo(() => { - const tuplesToRestore = [] - for (const tuple of values) { - const atom = tuple[0] - if (atom[hydratedSymbol] !== true) { - tuplesToRestore.push(tuple) - atom[hydratedSymbol] = true - } + const hydratedSet = getHydratedSet(store) + const tuplesToRestore = [] + for (const tuple of values) { + const atom = tuple[0] + if (!hydratedSet.has(atom)) { + hydratedSet.add(atom) + tuplesToRestore.push(tuple) } + } + if (tuplesToRestore.length) { restoreAtoms(tuplesToRestore) - }, [values, restoreAtoms]) + } +} + +function getHydratedSet(store: Store) { + let hydratedSet = hydratedMap.get(store) + if (!hydratedSet) { + hydratedSet = new Set() + hydratedMap.set(store, hydratedSet) + } + return hydratedSet } From 1808df7e31da34b473d1b8412872a98a7196ddf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 10:44:55 +0200 Subject: [PATCH 13/17] Add scope test --- tests/utils/useHydrateAtoms.test.tsx | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/utils/useHydrateAtoms.test.tsx b/tests/utils/useHydrateAtoms.test.tsx index d152e07896..dd30b6b204 100644 --- a/tests/utils/useHydrateAtoms.test.tsx +++ b/tests/utils/useHydrateAtoms.test.tsx @@ -199,11 +199,7 @@ it('useHydrateAtoms should respect onMount', async () => { useHydrateAtoms([[countAtom, initialCount]]) const [countValue] = useAtom(countAtom) - return ( - <> -
count: {countValue}
- - ) + return
count: {countValue}
} const { findByText } = render( @@ -214,3 +210,34 @@ it('useHydrateAtoms should respect onMount', async () => { await findByText('count: 42') expect(onMountFn).toBeCalledTimes(1) }) + +it('useHydrateAtoms should let you hydrate an atom once per scope', async () => { + const scope = Symbol() + const countAtom = atom(0) + + const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]]) + const [countValue] = useAtom(countAtom) + + return
count: {countValue}
+ } + const Counter2: FC<{ initialCount: number }> = ({ initialCount }) => { + useHydrateAtoms([[countAtom, initialCount]], scope) + const [countValue] = useAtom(countAtom) + + return
count: {countValue}
+ } + const { findByText } = render( + <> + + + + + + + + ) + + await findByText('count: 42') + await findByText('count: 65') +}) From 71b971c90e4fefb731986c534d19af1b3166698c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 10:52:01 +0200 Subject: [PATCH 14/17] Use new useAtom API for scope --- tests/utils/useHydrateAtoms.test.tsx | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/utils/useHydrateAtoms.test.tsx b/tests/utils/useHydrateAtoms.test.tsx index dd30b6b204..1f7a06245d 100644 --- a/tests/utils/useHydrateAtoms.test.tsx +++ b/tests/utils/useHydrateAtoms.test.tsx @@ -217,17 +217,29 @@ it('useHydrateAtoms should let you hydrate an atom once per scope', async () => const Counter: FC<{ initialCount: number }> = ({ initialCount }) => { useHydrateAtoms([[countAtom, initialCount]]) - const [countValue] = useAtom(countAtom) + const [countValue, setCount] = useAtom(countAtom) - return
count: {countValue}
+ return ( + <> +
count: {countValue}
+ + + ) } const Counter2: FC<{ initialCount: number }> = ({ initialCount }) => { useHydrateAtoms([[countAtom, initialCount]], scope) - const [countValue] = useAtom(countAtom) + const [countValue, setCount] = useAtom(countAtom, scope) - return
count: {countValue}
+ return ( + <> +
count: {countValue}
+ + + ) } - const { findByText } = render( + const { findByText, getByText } = render( <> @@ -240,4 +252,8 @@ it('useHydrateAtoms should let you hydrate an atom once per scope', async () => await findByText('count: 42') await findByText('count: 65') + fireEvent.click(getByText('dispatch')) + fireEvent.click(getByText('dispatch2')) + await findByText('count: 43') + await findByText('count: 66') }) From c1ee00ec9b169feab805cfb52a79e99178416a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 10:54:39 +0200 Subject: [PATCH 15/17] Update size-snapshot --- .size-snapshot.json | 68 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 1424273c12..7d54c8d0c4 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 21335, - "minified": 10169, - "gzipped": 3364, + "bundled": 21477, + "minified": 10278, + "gzipped": 3412, "treeshaked": { "rollup": { "code": 14, @@ -14,23 +14,23 @@ } }, "utils.js": { - "bundled": 16550, - "minified": 7958, - "gzipped": 2921, + "bundled": 15849, + "minified": 7524, + "gzipped": 2769, "treeshaked": { "rollup": { "code": 28, "import_statements": 28 }, "webpack": { - "code": 1314 + "code": 1317 } } }, "devtools.js": { - "bundled": 19190, - "minified": 9558, - "gzipped": 3288, + "bundled": 19055, + "minified": 9478, + "gzipped": 3259, "treeshaked": { "rollup": { "code": 28, @@ -42,9 +42,9 @@ } }, "immer.js": { - "bundled": 1637, - "minified": 856, - "gzipped": 418, + "bundled": 1615, + "minified": 844, + "gzipped": 407, "treeshaked": { "rollup": { "code": 42, @@ -56,9 +56,9 @@ } }, "optics.js": { - "bundled": 1684, - "minified": 828, - "gzipped": 440, + "bundled": 1646, + "minified": 812, + "gzipped": 429, "treeshaked": { "rollup": { "code": 32, @@ -70,9 +70,9 @@ } }, "query.js": { - "bundled": 5444, - "minified": 2279, - "gzipped": 710, + "bundled": 5106, + "minified": 2143, + "gzipped": 684, "treeshaked": { "rollup": { "code": 80, @@ -84,9 +84,9 @@ } }, "xstate.js": { - "bundled": 3565, - "minified": 1470, - "gzipped": 684, + "bundled": 2977, + "minified": 1314, + "gzipped": 653, "treeshaked": { "rollup": { "code": 29, @@ -98,9 +98,9 @@ } }, "valtio.js": { - "bundled": 1333, - "minified": 642, - "gzipped": 350, + "bundled": 1235, + "minified": 598, + "gzipped": 335, "treeshaked": { "rollup": { "code": 37, @@ -112,9 +112,9 @@ } }, "zustand.js": { - "bundled": 647, - "minified": 306, - "gzipped": 204, + "bundled": 549, + "minified": 262, + "gzipped": 188, "treeshaked": { "rollup": { "code": 14, @@ -126,9 +126,9 @@ } }, "redux.js": { - "bundled": 516, - "minified": 248, - "gzipped": 180, + "bundled": 458, + "minified": 220, + "gzipped": 167, "treeshaked": { "rollup": { "code": 14, @@ -140,9 +140,9 @@ } }, "urql.js": { - "bundled": 4539, - "minified": 2369, - "gzipped": 856, + "bundled": 4241, + "minified": 2259, + "gzipped": 837, "treeshaked": { "rollup": { "code": 170, From f75c4df0b4ac117ec60a355a4d46046d7dc0bd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 11:05:41 +0200 Subject: [PATCH 16/17] Fix isDevStore --- src/core/contexts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/contexts.ts b/src/core/contexts.ts index b15d830b42..48ecffa784 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -86,5 +86,5 @@ export const getStoreContext = (scope?: Scope) => { } export const isDevStore = (store: Store): store is StoreForDevelopment => { - return store.length > 3 + return store.length > 4 } From baece108c7345d1cecf56146a2ae4351b3982eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathis=20M=C3=B8ller?= Date: Tue, 10 Aug 2021 11:09:48 +0200 Subject: [PATCH 17/17] Fix typo - use weakset instead of set --- src/utils/useHydrateAtoms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 0dd8c5a1cd..c5f1babf9d 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -30,7 +30,7 @@ export function useHydrateAtoms( function getHydratedSet(store: Store) { let hydratedSet = hydratedMap.get(store) if (!hydratedSet) { - hydratedSet = new Set() + hydratedSet = new WeakSet() hydratedMap.set(store, hydratedSet) } return hydratedSet