| 1 | import { useStore } from 'jotai/react'; |
| 2 | import { useEffect, useLayoutEffect, useRef, useState } from 'react'; |
| 3 | import type { |
| 4 | AtomsDependents, |
| 5 | AtomsSnapshot, |
| 6 | AtomsValues, |
| 7 | Options, |
| 8 | Store, |
| 9 | } from '../types'; |
| 10 | |
| 11 | const isEqualAtomsValues = (left: AtomsValues, right: AtomsValues) => |
| 12 | left.size === right.size && |
| 13 | Array.from(left).every(([left, v]) => Object.is(right.get(left), v)); |
| 14 | |
| 15 | const isEqualAtomsDependents = ( |
| 16 | left: AtomsDependents, |
| 17 | right: AtomsDependents, |
| 18 | ) => |
| 19 | left.size === right.size && |
| 20 | Array.from(left).every(([a, dLeft]) => { |
| 21 | const dRight = right.get(a); |
| 22 | return ( |
| 23 | dRight && |
| 24 | dLeft.size === dRight.size && |
| 25 | Array.from(dLeft).every((d) => dRight.has(d)) |
| 26 | ); |
| 27 | }); |
| 28 | |
| 29 | export type SnapshotOptions = Options & { |
| 30 | /** |
| 31 | * Defaults to `false` |
| 32 | * |
| 33 | * Private are atoms that are used by Jotai libraries internally to manage state. |
| 34 | * They're often used internally in atoms like `atomWithStorage` or `atomWithLocation`, etc. to manage state. |
| 35 | */ |
| 36 | shouldShowPrivateAtoms?: boolean; |
| 37 | }; |
| 38 | |
| 39 | export function useAtomsSnapshot({ |
| 40 | shouldShowPrivateAtoms = false, |
| 41 | ...options |
| 42 | }: SnapshotOptions = {}): AtomsSnapshot { |
| 43 | const store = useStore(options); |
| 44 | |
| 45 | const [atomsSnapshot, setAtomsSnapshot] = useState<AtomsSnapshot>(() => ({ |
| 46 | values: new Map(), |
| 47 | dependents: new Map(), |
| 48 | })); |
| 49 | |
| 50 | const duringReactRenderPhase = useRef(true); |
| 51 | duringReactRenderPhase.current = true; |
| 52 | useLayoutEffect(() => { |
| 53 | duringReactRenderPhase.current = false; |
| 54 | }); |
| 55 | |
| 56 | useEffect(() => { |
| 57 | const devSubscribeStore: Store['dev_subscribe_store'] = |
| 58 | // @ts-expect-error dev_subscribe_state is deprecated in <= 2.0.3 |
| 59 | store?.dev_subscribe_store || store?.dev_subscribe_state; |
| 60 | |
| 61 | if (!devSubscribeStore) return; |
| 62 | |
| 63 | let prevValues: AtomsValues = new Map(); |
| 64 | let prevDependents: AtomsDependents = new Map(); |
| 65 | |
| 66 | if (!('dev_subscribe_store' in store)) { |
| 67 | console.warn( |
| 68 | '[DEPRECATION-WARNING]: Your Jotai version is out-of-date and contains deprecated properties that will be removed soon. Please update to the latest version of Jotai.', |
| 69 | ); |
| 70 | } |
| 71 | |
| 72 | // TODO remove this `t: any` and deprecation warnings in next breaking change release |
| 73 | const callback = ( |
| 74 | type?: Parameters<Parameters<typeof devSubscribeStore>[0]>[0], |
| 75 | ) => { |
| 76 | if (typeof type !== 'object') { |
| 77 | console.warn( |
| 78 | '[DEPRECATION-WARNING]: Your Jotai version is out-of-date and contains deprecated properties that will be removed soon. Please update to the latest version of Jotai.', |
| 79 | ); |
| 80 | } |
| 81 | |
| 82 | const values: AtomsValues = new Map(); |
| 83 | const dependents: AtomsDependents = new Map(); |
| 84 | for (const atom of store.dev_get_mounted_atoms?.() || []) { |
| 85 | if (!shouldShowPrivateAtoms && atom.debugPrivate) { |
| 86 | // Skip private atoms |
| 87 | continue; |
| 88 | } |
| 89 | |
| 90 | const atomState = store.dev_get_atom_state?.(atom); |
| 91 | if (atomState) { |
| 92 | if ('v' in atomState) { |
| 93 | values.set(atom, atomState.v); |
| 94 | } |
| 95 | } |
| 96 | const mounted = store.dev_get_mounted?.(atom); |
| 97 | if (mounted) { |
| 98 | let atomDependents = mounted.t; |
| 99 | |
| 100 | if (!shouldShowPrivateAtoms) { |
| 101 | // Filter private dependent atoms |
| 102 | atomDependents = new Set( |
| 103 | Array.from(atomDependents.values()).filter( |
| 104 | /* NOTE: This just removes private atoms from the dependents list, |
| 105 | instead of hiding them from the dependency chain and showing |
| 106 | the nested dependents of the private atoms. */ |
| 107 | (dependent) => !dependent.debugPrivate, |
| 108 | ), |
| 109 | ); |
| 110 | } |
| 111 | |
| 112 | dependents.set(atom, atomDependents); |
| 113 | } |
| 114 | } |
| 115 | if ( |
| 116 | isEqualAtomsValues(prevValues, values) && |
| 117 | isEqualAtomsDependents(prevDependents, dependents) |
| 118 | ) { |
| 119 | // not changed |
| 120 | return; |
| 121 | } |
| 122 | prevValues = values; |
| 123 | prevDependents = dependents; |
| 124 | const deferrableAtomSetAction = () => |
| 125 | setAtomsSnapshot({ values, dependents }); |
| 126 | if (duringReactRenderPhase.current) { |
| 127 | // avoid set action when react is rendering components |
| 128 | Promise.resolve().then(deferrableAtomSetAction); |
| 129 | } else { |
| 130 | deferrableAtomSetAction(); |
| 131 | } |
| 132 | }; |
| 133 | const unsubscribe = devSubscribeStore?.(callback, 2); |
| 134 | callback({} as any); |
| 135 | return unsubscribe; |
| 136 | }, [store, shouldShowPrivateAtoms]); |
| 137 | |
| 138 | return atomsSnapshot; |
| 139 | } |
| 140 | |