@tutti-os/ui-react-hooks 0.0.132 → 0.0.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,9 @@ hooks and UI components in their owning packages.
10
10
 
11
11
  Prefer this package over adding new direct `useSyncExternalStore` wrappers in
12
12
  shared frontend packages so subscription semantics stay consistent.
13
+ `useExternalStoreSnapshot` keeps its React subscription callbacks stable while
14
+ the source identity is unchanged, so source consumers do not unsubscribe and
15
+ resubscribe on each render.
13
16
 
14
17
  This package does not replace adapter-level snapshot memoization. If a non-React
15
18
  adapter exposes a derived `getSnapshot()`, keep reference-stable snapshot reuse
package/dist/index.js CHANGED
@@ -113,13 +113,18 @@ function useExternalStoreSelector(source, selector) {
113
113
  }
114
114
 
115
115
  // src/useExternalStoreSnapshot.ts
116
- import { useSyncExternalStore as useSyncExternalStore2 } from "react";
116
+ import { useCallback, useSyncExternalStore as useSyncExternalStore2 } from "react";
117
117
  function useExternalStoreSnapshot(source) {
118
- return useSyncExternalStore2(
118
+ const subscribe = useCallback(
119
119
  (listener) => source.subscribe(listener),
120
- () => source.getSnapshot(),
121
- () => (source.getServerSnapshot ?? source.getSnapshot)()
120
+ [source]
122
121
  );
122
+ const getSnapshot = useCallback(() => source.getSnapshot(), [source]);
123
+ const getServerSnapshot = useCallback(
124
+ () => (source.getServerSnapshot ?? source.getSnapshot)(),
125
+ [source]
126
+ );
127
+ return useSyncExternalStore2(subscribe, getSnapshot, getServerSnapshot);
123
128
  }
124
129
  export {
125
130
  resolveComposedInputValueSync,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/useComposedInputValue.ts","../src/useExternalStoreSelector.ts","../src/useExternalStoreSnapshot.ts"],"sourcesContent":["import {\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type CompositionEvent,\n type FocusEvent\n} from \"react\";\n\nexport interface ComposedInputPendingCommit {\n committedValue: string;\n previousValue: string;\n}\n\nexport interface ComposedInputValueSyncInput {\n isComposing: boolean;\n localValue: string;\n pendingCommit: ComposedInputPendingCommit | null;\n value: string;\n}\n\nexport interface ComposedInputValueSyncResult {\n pendingCommit: ComposedInputPendingCommit | null;\n shouldSyncLocalValue: boolean;\n value: string;\n}\n\nexport interface UseComposedInputValueInput {\n onCommit: (value: string) => void;\n value: string;\n}\n\nexport interface UseComposedInputValueResult {\n clearValue: () => void;\n commitValue: (value: string) => void;\n isComposing: boolean;\n onBlur: (event: FocusEvent<HTMLInputElement>) => void;\n onChange: (event: ChangeEvent<HTMLInputElement>) => void;\n onCompositionEnd: (event: CompositionEvent<HTMLInputElement>) => void;\n onCompositionStart: () => void;\n value: string;\n}\n\nexport function resolveComposedInputValueSync(\n input: ComposedInputValueSyncInput\n): ComposedInputValueSyncResult {\n if (input.isComposing) {\n return {\n pendingCommit: input.pendingCommit,\n shouldSyncLocalValue: false,\n value: input.localValue\n };\n }\n\n if (input.pendingCommit !== null) {\n if (input.value === input.pendingCommit.committedValue) {\n return {\n pendingCommit: null,\n shouldSyncLocalValue: input.localValue !== input.value,\n value: input.value\n };\n }\n\n if (\n input.localValue === input.pendingCommit.committedValue &&\n input.value === input.pendingCommit.previousValue\n ) {\n return {\n pendingCommit: input.pendingCommit,\n shouldSyncLocalValue: false,\n value: input.localValue\n };\n }\n }\n\n return {\n pendingCommit: null,\n shouldSyncLocalValue: input.localValue !== input.value,\n value: input.value\n };\n}\n\nexport function useComposedInputValue({\n onCommit,\n value\n}: UseComposedInputValueInput): UseComposedInputValueResult {\n const pendingCommitRef = useRef<ComposedInputPendingCommit | null>(null);\n const isComposingRef = useRef(false);\n const [localValue, setLocalValue] = useState(value);\n const [isComposing, setIsComposing] = useState(false);\n\n useEffect(() => {\n const syncResult = resolveComposedInputValueSync({\n isComposing,\n localValue,\n pendingCommit: pendingCommitRef.current,\n value\n });\n\n pendingCommitRef.current = syncResult.pendingCommit;\n if (syncResult.shouldSyncLocalValue) {\n setLocalValue(syncResult.value);\n }\n }, [isComposing, localValue, value]);\n\n const commitValue = (nextValue: string) => {\n pendingCommitRef.current = {\n committedValue: nextValue,\n previousValue: value\n };\n setLocalValue(nextValue);\n onCommit(nextValue);\n };\n\n return {\n clearValue: () => {\n commitValue(\"\");\n },\n commitValue,\n isComposing,\n onBlur: (event: FocusEvent<HTMLInputElement>) => {\n commitValue(event.currentTarget.value);\n },\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const nextValue = event.currentTarget.value;\n if (!isComposingRef.current) {\n commitValue(nextValue);\n return;\n }\n setLocalValue(nextValue);\n },\n onCompositionEnd: (event: CompositionEvent<HTMLInputElement>) => {\n isComposingRef.current = false;\n setIsComposing(false);\n commitValue(event.currentTarget.value);\n },\n onCompositionStart: () => {\n isComposingRef.current = true;\n setIsComposing(true);\n },\n value: localValue\n };\n}\n","import { useRef, useSyncExternalStore } from \"react\";\nimport type { ExternalStoreSnapshotSource } from \"./types.ts\";\n\nexport function useExternalStoreSelector<TSnapshot, TResult>(\n source: ExternalStoreSnapshotSource<TSnapshot>,\n selector: (snapshot: TSnapshot) => TResult\n): TResult {\n const selectedSnapshotRef = useRef<{\n selector: (snapshot: TSnapshot) => TResult;\n snapshot: TSnapshot;\n value: TResult;\n } | null>(null);\n\n const readSelection = (snapshot: TSnapshot) => {\n const selectedSnapshot = selectedSnapshotRef.current;\n if (\n selectedSnapshot?.snapshot === snapshot &&\n selectedSnapshot.selector === selector\n ) {\n return selectedSnapshot.value;\n }\n\n const value = selector(snapshot);\n selectedSnapshotRef.current = { selector, snapshot, value };\n return value;\n };\n\n return useSyncExternalStore(\n (listener) => source.subscribe(listener),\n () => readSelection(source.getSnapshot()),\n () => readSelection((source.getServerSnapshot ?? source.getSnapshot)())\n );\n}\n","import { useSyncExternalStore } from \"react\";\nimport type { ExternalStoreSnapshotSource } from \"./types.ts\";\n\nexport function useExternalStoreSnapshot<TSnapshot>(\n source: ExternalStoreSnapshotSource<TSnapshot>\n): TSnapshot {\n return useSyncExternalStore(\n (listener) => source.subscribe(listener),\n () => source.getSnapshot(),\n () => (source.getServerSnapshot ?? source.getSnapshot)()\n );\n}\n"],"mappings":"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","names":["useRef","useSyncExternalStore"]}
1
+ {"version":3,"sources":["../src/useComposedInputValue.ts","../src/useExternalStoreSelector.ts","../src/useExternalStoreSnapshot.ts"],"sourcesContent":["import {\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type CompositionEvent,\n type FocusEvent\n} from \"react\";\n\nexport interface ComposedInputPendingCommit {\n committedValue: string;\n previousValue: string;\n}\n\nexport interface ComposedInputValueSyncInput {\n isComposing: boolean;\n localValue: string;\n pendingCommit: ComposedInputPendingCommit | null;\n value: string;\n}\n\nexport interface ComposedInputValueSyncResult {\n pendingCommit: ComposedInputPendingCommit | null;\n shouldSyncLocalValue: boolean;\n value: string;\n}\n\nexport interface UseComposedInputValueInput {\n onCommit: (value: string) => void;\n value: string;\n}\n\nexport interface UseComposedInputValueResult {\n clearValue: () => void;\n commitValue: (value: string) => void;\n isComposing: boolean;\n onBlur: (event: FocusEvent<HTMLInputElement>) => void;\n onChange: (event: ChangeEvent<HTMLInputElement>) => void;\n onCompositionEnd: (event: CompositionEvent<HTMLInputElement>) => void;\n onCompositionStart: () => void;\n value: string;\n}\n\nexport function resolveComposedInputValueSync(\n input: ComposedInputValueSyncInput\n): ComposedInputValueSyncResult {\n if (input.isComposing) {\n return {\n pendingCommit: input.pendingCommit,\n shouldSyncLocalValue: false,\n value: input.localValue\n };\n }\n\n if (input.pendingCommit !== null) {\n if (input.value === input.pendingCommit.committedValue) {\n return {\n pendingCommit: null,\n shouldSyncLocalValue: input.localValue !== input.value,\n value: input.value\n };\n }\n\n if (\n input.localValue === input.pendingCommit.committedValue &&\n input.value === input.pendingCommit.previousValue\n ) {\n return {\n pendingCommit: input.pendingCommit,\n shouldSyncLocalValue: false,\n value: input.localValue\n };\n }\n }\n\n return {\n pendingCommit: null,\n shouldSyncLocalValue: input.localValue !== input.value,\n value: input.value\n };\n}\n\nexport function useComposedInputValue({\n onCommit,\n value\n}: UseComposedInputValueInput): UseComposedInputValueResult {\n const pendingCommitRef = useRef<ComposedInputPendingCommit | null>(null);\n const isComposingRef = useRef(false);\n const [localValue, setLocalValue] = useState(value);\n const [isComposing, setIsComposing] = useState(false);\n\n useEffect(() => {\n const syncResult = resolveComposedInputValueSync({\n isComposing,\n localValue,\n pendingCommit: pendingCommitRef.current,\n value\n });\n\n pendingCommitRef.current = syncResult.pendingCommit;\n if (syncResult.shouldSyncLocalValue) {\n setLocalValue(syncResult.value);\n }\n }, [isComposing, localValue, value]);\n\n const commitValue = (nextValue: string) => {\n pendingCommitRef.current = {\n committedValue: nextValue,\n previousValue: value\n };\n setLocalValue(nextValue);\n onCommit(nextValue);\n };\n\n return {\n clearValue: () => {\n commitValue(\"\");\n },\n commitValue,\n isComposing,\n onBlur: (event: FocusEvent<HTMLInputElement>) => {\n commitValue(event.currentTarget.value);\n },\n onChange: (event: ChangeEvent<HTMLInputElement>) => {\n const nextValue = event.currentTarget.value;\n if (!isComposingRef.current) {\n commitValue(nextValue);\n return;\n }\n setLocalValue(nextValue);\n },\n onCompositionEnd: (event: CompositionEvent<HTMLInputElement>) => {\n isComposingRef.current = false;\n setIsComposing(false);\n commitValue(event.currentTarget.value);\n },\n onCompositionStart: () => {\n isComposingRef.current = true;\n setIsComposing(true);\n },\n value: localValue\n };\n}\n","import { useRef, useSyncExternalStore } from \"react\";\nimport type { ExternalStoreSnapshotSource } from \"./types.ts\";\n\nexport function useExternalStoreSelector<TSnapshot, TResult>(\n source: ExternalStoreSnapshotSource<TSnapshot>,\n selector: (snapshot: TSnapshot) => TResult\n): TResult {\n const selectedSnapshotRef = useRef<{\n selector: (snapshot: TSnapshot) => TResult;\n snapshot: TSnapshot;\n value: TResult;\n } | null>(null);\n\n const readSelection = (snapshot: TSnapshot) => {\n const selectedSnapshot = selectedSnapshotRef.current;\n if (\n selectedSnapshot?.snapshot === snapshot &&\n selectedSnapshot.selector === selector\n ) {\n return selectedSnapshot.value;\n }\n\n const value = selector(snapshot);\n selectedSnapshotRef.current = { selector, snapshot, value };\n return value;\n };\n\n return useSyncExternalStore(\n (listener) => source.subscribe(listener),\n () => readSelection(source.getSnapshot()),\n () => readSelection((source.getServerSnapshot ?? source.getSnapshot)())\n );\n}\n","import { useCallback, useSyncExternalStore } from \"react\";\nimport type { ExternalStoreSnapshotSource } from \"./types.ts\";\n\nexport function useExternalStoreSnapshot<TSnapshot>(\n source: ExternalStoreSnapshotSource<TSnapshot>\n): TSnapshot {\n const subscribe = useCallback(\n (listener: () => void) => source.subscribe(listener),\n [source]\n );\n const getSnapshot = useCallback(() => source.getSnapshot(), [source]);\n const getServerSnapshot = useCallback(\n () => (source.getServerSnapshot ?? source.getSnapshot)(),\n [source]\n );\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"],"mappings":"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","names":["useRef","useSyncExternalStore"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tutti-os/ui-react-hooks",
3
- "version": "0.0.132",
3
+ "version": "0.0.134",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",