From 5e4fa31fb74c673805ed6c7fb360b9c44f6902f2 Mon Sep 17 00:00:00 2001 From: PrettyCoffee Date: Sun, 26 Feb 2023 17:56:24 +0100 Subject: [PATCH 1/2] refactor(utils): split redux connection init from subscribing --- src/utils/useAtomDevtools.ts | 39 +++++++++++++++---------------- src/utils/useAtomsDevtools.ts | 44 +++++++++++++++++------------------ 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/utils/useAtomDevtools.ts b/src/utils/useAtomDevtools.ts index e7257338..531b78ac 100644 --- a/src/utils/useAtomDevtools.ts +++ b/src/utils/useAtomDevtools.ts @@ -18,20 +18,22 @@ export function useAtomDevtools( ): void { const { enabled, name } = options || {}; - const extension = getReduxExtension(enabled); - const [value, setValue] = useAtom(anAtom, options); const lastValue = useRef(value); const isTimeTraveling = useRef(false); - const devtools = useRef(); + const connection = useRef(); const atomName = name || anAtom.debugLabel || anAtom.toString(); useEffect(() => { - if (!extension) { - return; - } + const extension = getReduxExtension(enabled); + connection.current = createReduxConnection(extension, atomName); + }, [atomName, enabled]); + + useEffect(() => { + if (!connection.current) return; + const setValueIfWritable = (value: Value) => { if (typeof setValue === 'function') { (setValue as (value: Value) => void)(value); @@ -43,9 +45,7 @@ export function useAtomDevtools( ); }; - devtools.current = createReduxConnection(extension, atomName); - - const unsubscribe = devtools.current?.subscribe((message) => { + const unsubscribe = connection.current.subscribe((message) => { if (message.type === 'ACTION' && message.payload) { try { setValueIfWritable(JSON.parse(message.payload)); @@ -68,7 +68,7 @@ export function useAtomDevtools( message.type === 'DISPATCH' && message.payload?.type === 'COMMIT' ) { - devtools.current?.init(lastValue.current); + connection.current?.init(lastValue.current); } else if ( message.type === 'DISPATCH' && message.payload?.type === 'IMPORT_STATE' @@ -78,7 +78,7 @@ export function useAtomDevtools( computedStates.forEach(({ state }: { state: Value }, index: number) => { if (index === 0) { - devtools.current?.init(state); + connection.current?.init(state); } else { setValueIfWritable(state); } @@ -87,23 +87,22 @@ export function useAtomDevtools( }); return unsubscribe; - }, [anAtom, extension, atomName, setValue]); + }, [anAtom, setValue]); useEffect(() => { - if (!devtools.current) { - return; - } + if (!connection.current) return; + lastValue.current = value; - if (devtools.current.shouldInit) { - devtools.current.init(value); - devtools.current.shouldInit = false; + if (connection.current.shouldInit) { + connection.current.init(value); + connection.current.shouldInit = false; } else if (isTimeTraveling.current) { isTimeTraveling.current = false; } else { - devtools.current.send( + connection.current.send( `${atomName} - ${new Date().toLocaleString()}` as any, value, ); } - }, [anAtom, extension, atomName, value]); + }, [atomName, value]); } diff --git a/src/utils/useAtomsDevtools.ts b/src/utils/useAtomsDevtools.ts index cf8f2564..dd9d1c05 100644 --- a/src/utils/useAtomsDevtools.ts +++ b/src/utils/useAtomsDevtools.ts @@ -36,22 +36,28 @@ export function useAtomsDevtools( ): void { const { enabled } = options || {}; - const extension = getReduxExtension(enabled); - // This an exception, we don't usually use utils in themselves! const atomsSnapshot = useAtomsSnapshot(options); const goToSnapshot = useGotoAtomsSnapshot(options); const isTimeTraveling = useRef(false); const isRecording = useRef(true); - const devtools = useRef(); + const connection = useRef(); const snapshots = useRef([]); useEffect(() => { - if (!extension) { - return; - } + const extension = getReduxExtension(enabled); + connection.current = createReduxConnection(extension, name); + + return () => { + extension?.disconnect?.(); + }; + }, [enabled, name]); + + useEffect(() => { + if (!connection.current) return; + const getSnapshotAt = (index = snapshots.current.length - 1) => { // index 0 is @@INIT, so we need to return the next action (0) const snapshot = snapshots.current[index >= 0 ? index : 0]; @@ -61,9 +67,7 @@ export function useAtomsDevtools( return snapshot; }; - devtools.current = createReduxConnection(extension, name); - - const devtoolsUnsubscribe = devtools.current?.subscribe((message) => { + const unsubscribe = connection.current.subscribe((message) => { switch (message.type) { case 'DISPATCH': switch (message.payload?.type) { @@ -72,7 +76,7 @@ export function useAtomsDevtools( break; case 'COMMIT': - devtools.current?.init(getDevtoolsState(getSnapshotAt())); + connection.current?.init(getDevtoolsState(getSnapshotAt())); snapshots.current = []; break; @@ -89,26 +93,22 @@ export function useAtomsDevtools( } }); - return () => { - extension?.disconnect?.(); - devtoolsUnsubscribe?.(); - }; - }, [extension, goToSnapshot, name]); + return unsubscribe; + }, [goToSnapshot]); useEffect(() => { - if (!devtools.current) { - return; - } - if (devtools.current.shouldInit) { - devtools.current.init(undefined); - devtools.current.shouldInit = false; + if (!connection.current) return; + + if (connection.current.shouldInit) { + connection.current.init(undefined); + connection.current.shouldInit = false; return; } if (isTimeTraveling.current) { isTimeTraveling.current = false; } else if (isRecording.current) { snapshots.current.push(atomsSnapshot); - devtools.current.send( + connection.current.send( { type: `${snapshots.current.length}`, updatedAt: new Date().toLocaleString(), From 37197c8c35b998f730443c19803bb8da88ec6d80 Mon Sep 17 00:00:00 2001 From: PrettyCoffee Date: Sun, 19 Mar 2023 13:46:29 +0100 Subject: [PATCH 2/2] refactor(utils): extract useReduxConnection for less repetition --- .../redux-extension/createReduxConnection.ts | 7 +--- .../redux-extension/useReduxConnection.ts | 34 ++++++++++++++++++ src/utils/useAtomDevtools.ts | 29 +++++++-------- src/utils/useAtomsDevtools.ts | 35 +++++++------------ src/utils/useDidMount.ts | 9 +++++ 5 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 src/utils/redux-extension/useReduxConnection.ts create mode 100644 src/utils/useDidMount.ts diff --git a/src/utils/redux-extension/createReduxConnection.ts b/src/utils/redux-extension/createReduxConnection.ts index f2bb2b36..143eb07b 100644 --- a/src/utils/redux-extension/createReduxConnection.ts +++ b/src/utils/redux-extension/createReduxConnection.ts @@ -5,9 +5,6 @@ import { ReduxExtension } from './getReduxExtension'; type ConnectResponse = ReturnType['connect']>; export type Connection = { - /** Mark the connection as not initiated, so it can be initiated before using it. */ - shouldInit?: boolean; - /** Initiate the connection and add it to the extension connections. * Should only be executed once in the live time of the connection. */ @@ -40,7 +37,5 @@ export const createReduxConnection = ( if (!extension) return undefined; const connection = extension.connect({ name }); - return Object.assign(connection, { - shouldInit: true, - }) as Connection; + return connection as Connection; }; diff --git a/src/utils/redux-extension/useReduxConnection.ts b/src/utils/redux-extension/useReduxConnection.ts new file mode 100644 index 00000000..c43172c3 --- /dev/null +++ b/src/utils/redux-extension/useReduxConnection.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef } from 'react'; +import { Connection, createReduxConnection } from './createReduxConnection'; +import { getReduxExtension } from './getReduxExtension'; + +interface ConnectionConfig { + name: string; + enabled: boolean | undefined; + initialValue: T; + disconnectAllOnCleanup?: boolean; +} + +export const useReduxConnection = ({ + name, + initialValue, + enabled, + disconnectAllOnCleanup, +}: ConnectionConfig) => { + const connectionRef = useRef(); + const firstValue = useRef(initialValue); + + useEffect(() => { + const extension = getReduxExtension(enabled); + + const connection = createReduxConnection(extension, name); + connection?.init(firstValue.current); + connectionRef.current = connection; + + return () => { + if (disconnectAllOnCleanup) extension?.disconnect?.(); + }; + }, [name, enabled, disconnectAllOnCleanup]); + + return connectionRef; +}; diff --git a/src/utils/useAtomDevtools.ts b/src/utils/useAtomDevtools.ts index 531b78ac..16e9eafb 100644 --- a/src/utils/useAtomDevtools.ts +++ b/src/utils/useAtomDevtools.ts @@ -1,11 +1,8 @@ import { useEffect, useRef } from 'react'; import { useAtom } from 'jotai/react'; import type { Atom, WritableAtom } from 'jotai/vanilla'; -import { - Connection, - createReduxConnection, -} from './redux-extension/createReduxConnection'; -import { getReduxExtension } from './redux-extension/getReduxExtension'; +import { useReduxConnection } from './redux-extension/useReduxConnection'; +import { useDidMount } from './useDidMount'; type DevtoolOptions = Parameters[1] & { name?: string; @@ -17,19 +14,20 @@ export function useAtomDevtools( options?: DevtoolOptions, ): void { const { enabled, name } = options || {}; + const didMount = useDidMount(); const [value, setValue] = useAtom(anAtom, options); const lastValue = useRef(value); const isTimeTraveling = useRef(false); - const connection = useRef(); const atomName = name || anAtom.debugLabel || anAtom.toString(); - useEffect(() => { - const extension = getReduxExtension(enabled); - connection.current = createReduxConnection(extension, atomName); - }, [atomName, enabled]); + const connection = useReduxConnection({ + name: atomName, + enabled, + initialValue: value, + }); useEffect(() => { if (!connection.current) return; @@ -87,16 +85,13 @@ export function useAtomDevtools( }); return unsubscribe; - }, [anAtom, setValue]); + }, [anAtom, connection, setValue]); useEffect(() => { - if (!connection.current) return; + if (!connection.current || !didMount) return; lastValue.current = value; - if (connection.current.shouldInit) { - connection.current.init(value); - connection.current.shouldInit = false; - } else if (isTimeTraveling.current) { + if (isTimeTraveling.current) { isTimeTraveling.current = false; } else { connection.current.send( @@ -104,5 +99,5 @@ export function useAtomDevtools( value, ); } - }, [atomName, value]); + }, [atomName, connection, didMount, value]); } diff --git a/src/utils/useAtomsDevtools.ts b/src/utils/useAtomsDevtools.ts index dd9d1c05..d496b338 100644 --- a/src/utils/useAtomsDevtools.ts +++ b/src/utils/useAtomsDevtools.ts @@ -1,11 +1,8 @@ import { useEffect, useRef } from 'react'; import { AnyAtom, AnyAtomValue, AtomsSnapshot, Options } from '../types'; -import { - Connection, - createReduxConnection, -} from './redux-extension/createReduxConnection'; -import { getReduxExtension } from './redux-extension/getReduxExtension'; +import { useReduxConnection } from './redux-extension/useReduxConnection'; import { useAtomsSnapshot } from './useAtomsSnapshot'; +import { useDidMount } from './useDidMount'; import { useGotoAtomsSnapshot } from './useGotoAtomsSnapshot'; const atomToPrintable = (atom: AnyAtom) => @@ -35,6 +32,7 @@ export function useAtomsDevtools( options?: DevtoolsOptions, ): void { const { enabled } = options || {}; + const didMount = useDidMount(); // This an exception, we don't usually use utils in themselves! const atomsSnapshot = useAtomsSnapshot(options); @@ -42,18 +40,14 @@ export function useAtomsDevtools( const isTimeTraveling = useRef(false); const isRecording = useRef(true); - const connection = useRef(); - const snapshots = useRef([]); - useEffect(() => { - const extension = getReduxExtension(enabled); - connection.current = createReduxConnection(extension, name); - - return () => { - extension?.disconnect?.(); - }; - }, [enabled, name]); + const connection = useReduxConnection({ + name, + enabled, + initialValue: undefined, + disconnectAllOnCleanup: true, + }); useEffect(() => { if (!connection.current) return; @@ -94,16 +88,11 @@ export function useAtomsDevtools( }); return unsubscribe; - }, [goToSnapshot]); + }, [connection, goToSnapshot]); useEffect(() => { - if (!connection.current) return; + if (!connection.current || !didMount) return; - if (connection.current.shouldInit) { - connection.current.init(undefined); - connection.current.shouldInit = false; - return; - } if (isTimeTraveling.current) { isTimeTraveling.current = false; } else if (isRecording.current) { @@ -116,5 +105,5 @@ export function useAtomsDevtools( getDevtoolsState(atomsSnapshot), ); } - }, [atomsSnapshot]); + }, [atomsSnapshot, connection, didMount]); } diff --git a/src/utils/useDidMount.ts b/src/utils/useDidMount.ts new file mode 100644 index 00000000..2dd33c28 --- /dev/null +++ b/src/utils/useDidMount.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from 'react'; + +export const useDidMount = () => { + const didMount = useRef(false); + useEffect(() => { + didMount.current = true; + }, []); + return didMount.current; +};