@tanstack/react-optimistic
Version:
React hooks for optimistic updates
1 lines • 16.6 kB
Source Map (JSON)
{"version":3,"file":"useCollection.cjs","sources":["../../src/useCollection.ts"],"sourcesContent":["import { useSyncExternalStoreWithSelector } from \"use-sync-external-store/shim/with-selector.js\"\nimport {\n Collection,\n collectionsStore,\n preloadCollection,\n} from \"@tanstack/optimistic\"\nimport type {\n CollectionConfig,\n SortedMap,\n Transaction,\n} from \"@tanstack/optimistic\"\n\nexport { preloadCollection }\n\n// Cache for snapshots to prevent infinite loops\nlet snapshotCache: Map<\n string,\n { state: Map<unknown, unknown>; transactions: SortedMap<string, Transaction> }\n> | null = null\n\n/**\n * Hook that provides access to all collections\n *\n * @returns A Map of all collections with their states and transactions\n */\nexport function useCollections() {\n return useSyncExternalStoreWithSelector(\n (callback) => {\n // Subscribe to the collections store for new collections\n const storeUnsubscribe = collectionsStore.subscribe(() => {\n snapshotCache = null // Invalidate cache when collections change\n callback()\n })\n\n // Subscribe to all collections' derived states and transactions\n const collectionUnsubscribes = Array.from(\n collectionsStore.state.values()\n ).map((collection) => {\n const derivedStateUnsub = collection.derivedState.subscribe(() => {\n snapshotCache = null // Invalidate cache when state changes\n callback()\n })\n\n return derivedStateUnsub\n })\n\n return () => {\n storeUnsubscribe()\n collectionUnsubscribes.forEach((unsubscribe) => unsubscribe())\n }\n },\n // Get snapshot of all collections and their transactions\n () => {\n if (snapshotCache) {\n return snapshotCache\n }\n\n const snapshot = new Map<\n string,\n {\n state: Map<unknown, unknown>\n transactions: SortedMap<string, Transaction>\n }\n >()\n for (const [id, collection] of collectionsStore.state) {\n snapshot.set(id, {\n state: collection.derivedState.state,\n transactions: collection.transactions.state,\n })\n }\n snapshotCache = snapshot\n return snapshot\n },\n // Server snapshot (same as client for now)\n () => {\n if (snapshotCache) {\n return snapshotCache\n }\n\n const snapshot = new Map<\n string,\n {\n state: Map<unknown, unknown>\n transactions: SortedMap<string, Transaction>\n }\n >()\n for (const [id, collection] of collectionsStore.state) {\n snapshot.set(id, {\n state: collection.derivedState.state,\n transactions: collection.transactions.state,\n })\n }\n snapshotCache = snapshot\n return snapshot\n },\n // Identity selector\n (state) => state,\n // Custom equality function for Maps\n (a, b) => {\n if (a === b) return true\n if (!(a instanceof Map) || !(b instanceof Map)) return false\n if (a.size !== b.size) return false\n for (const [key, value] of a) {\n const bValue = b.get(key)\n if (!b.has(key)) return false\n if (value instanceof Map) {\n if (!(bValue instanceof Map)) return false\n if (!shallow(value, bValue)) return false\n } else if (!Object.is(value, bValue)) {\n return false\n }\n }\n return true\n }\n )\n}\n\n/**\n * Hook to use a specific collection with React\n *\n * @template T - Type of data in the collection\n * @template R - Return type of the selector function\n * @param config - Configuration for the collection\n * @param selector - TODO Optional selector function to transform the collection data\n * @returns Object containing collection data and CRUD operations\n */\n// Overload for when selector is not provided\n\nexport function useCollection<T extends object>(\n config: CollectionConfig<T>\n): {\n /**\n * The collection data as a Map with keys as identifiers\n */\n state: Map<string, T>\n /**\n * The collection data as an Array of data\n */\n data: Array<T>\n /**\n * Updates an existing item in the collection\n *\n * @param item - The item to update (must exist in collection)\n * @param configOrCallback - Update configuration or callback function\n * @param maybeCallback - Callback function if config was provided\n * @returns {Transaction} A Transaction object representing the update operation\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Update a single item\n * update(todo, (draft) => { draft.completed = true })\n *\n * // Update multiple items\n * update([todo1, todo2], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n *\n * // Update with metadata\n * update(todo, { metadata: { reason: \"user update\" } }, (draft) => { draft.text = \"Updated text\" })\n */\n update: Collection<T>[`update`]\n /**\n * Inserts a new item or items into the collection\n *\n * @param data - Single item or array of items to insert\n * @param config - Optional configuration including key(s) and metadata\n * @returns {Transaction} A Transaction object representing the insert operation\n * @throws {SchemaValidationError} If the data fails schema validation\n * @throws {Error} If more keys provided than items to insert\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Insert a single item\n * insert({ text: \"Buy groceries\", completed: false })\n *\n * // Insert multiple items\n * insert([\n * { text: \"Buy groceries\", completed: false },\n * { text: \"Walk dog\", completed: false }\n * ])\n *\n * // Insert with custom key\n * insert({ text: \"Buy groceries\" }, { key: \"grocery-task\" })\n */\n insert: Collection<T>[`insert`]\n /**\n * Deletes an item or items from the collection\n *\n * @param items - Item(s) to delete (must exist in collection) or their key(s)\n * @param config - Optional configuration including metadata\n * @returns {Transaction} A Transaction object representing the delete operation\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Delete a single item\n * delete(todo)\n *\n * // Delete multiple items\n * delete([todo1, todo2])\n *\n * // Delete with metadata\n * delete(todo, { metadata: { reason: \"completed\" } })\n */\n delete: Collection<T>[`delete`]\n}\n\n// Overload for when selector is provided\n// eslint-disable-next-line\nexport function useCollection<T extends object, R>(\n config: CollectionConfig<T>,\n selector: (d: Map<string, T>) => R\n): {\n /**\n * The collection data as a Map with keys as identifiers\n */\n state: Map<string, T>\n /**\n * The collection data as an Array of items\n */\n data: Array<T>\n /**\n * Updates an existing item in the collection\n *\n * @param item - The item to update (must exist in collection)\n * @param configOrCallback - Update configuration or callback function\n * @param maybeCallback - Callback function if config was provided\n * @returns {Transaction} A Transaction object representing the update operation\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Update a single item\n * update(todo, (draft) => { draft.completed = true })\n *\n * // Update multiple items\n * update([todo1, todo2], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n *\n * // Update with metadata\n * update(todo, { metadata: { reason: \"user update\" } }, (draft) => { draft.text = \"Updated text\" })\n */\n update: Collection<T>[`update`]\n /**\n * Inserts a new item or items into the collection\n *\n * @param data - Single item or array of items to insert\n * @param config - Optional configuration including key(s) and metadata\n * @returns {Transaction} A Transaction object representing the insert operation\n * @throws {SchemaValidationError} If the data fails schema validation\n * @throws {Error} If more keys provided than items to insert\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Insert a single item\n * insert({ text: \"Buy groceries\", completed: false })\n *\n * // Insert multiple items\n * insert([\n * { text: \"Buy groceries\", completed: false },\n * { text: \"Walk dog\", completed: false }\n * ])\n *\n * // Insert with custom key\n * insert({ text: \"Buy groceries\" }, { key: \"grocery-task\" })\n */\n insert: Collection<T>[`insert`]\n /**\n * Deletes an item or items from the collection\n *\n * @param items - Item(s) to delete (must exist in collection) or their key(s)\n * @param config - Optional configuration including metadata\n * @returns {Transaction} A Transaction object representing the delete operation\n * @throws {Error} If mutationFn is not provided in the collection config\n * @example\n * // Delete a single item\n * delete(todo)\n *\n * // Delete multiple items\n * delete([todo1, todo2])\n *\n * // Delete with metadata\n * delete(todo, { metadata: { reason: \"completed\" } })\n */\n delete: Collection<T>[`delete`]\n}\n\n// Implementation\n// eslint-disable-next-line\nexport function useCollection<T extends object, R = any>(\n config: CollectionConfig<T>,\n selector?: (d: Map<string, T>) => R\n) {\n if (selector) {\n console.log(`selector support not yet implemented`, selector)\n }\n // Get or create collection instance\n if (!collectionsStore.state.has(config.id)) {\n // If collection doesn't exist yet, create it\n // This will reuse any existing collection created by preloadCollection\n collectionsStore.setState((prev) => {\n const next = new Map(prev)\n next.set(\n config.id,\n new Collection<T>({\n id: config.id,\n sync: config.sync,\n schema: config.schema,\n })\n )\n return next\n })\n }\n\n const collection = collectionsStore.state.get(config.id)! as Collection<T>\n\n // Use a single subscription to get all the data we need\n const result = useSyncExternalStoreWithSelector<\n Map<string, T>,\n { state: Map<string, T>; data: Array<T> }\n >(\n collection.derivedState.subscribe,\n () => collection.derivedState.state,\n () => collection.derivedState.state,\n (stateMap) => {\n return {\n state: stateMap,\n // derivedState & derivedArray are recomputed at the same time.\n data: collection.derivedArray.state,\n }\n },\n (a, b) => {\n // Custom equality function that checks each property\n if (a === b) return true\n\n // Check if state maps are equal\n const stateEqual =\n a.state.size === b.state.size &&\n Array.from(a.state.keys()).every((key) =>\n shallow(a.state.get(key), b.state.get(key))\n )\n\n // Check if data arrays are equal\n const dataEqual =\n a.data.length === b.data.length &&\n a.data.every((datum, i) => shallow(datum, b.data[i]))\n\n return stateEqual && dataEqual\n }\n )\n\n const returnValue = {\n state: result.state,\n data: result.data,\n insert: collection.insert.bind(collection),\n update: collection.update.bind(collection),\n delete: collection.delete.bind(collection),\n }\n\n return returnValue\n}\n\n/**\n * Performs a shallow comparison between two objects\n * Used for equality checking in React hooks\n *\n * @template T - Type of objects to compare\n * @param objA - First object\n * @param objB - Second object\n * @returns True if objects are shallowly equal, false otherwise\n */\nexport function shallow<T>(objA: T, objB: T) {\n if (Object.is(objA, objB)) {\n return true\n }\n\n if (\n typeof objA !== `object` ||\n objA === null ||\n typeof objB !== `object` ||\n objB === null\n ) {\n return false\n }\n\n if (objA instanceof Map && objB instanceof Map) {\n if (objA.size !== objB.size) return false\n for (const [k, v] of objA) {\n if (!objB.has(k) || !Object.is(v, objB.get(k))) return false\n }\n return true\n }\n\n if (objA instanceof Set && objB instanceof Set) {\n if (objA.size !== objB.size) return false\n for (const v of objA) {\n if (!objB.has(v)) return false\n }\n return true\n }\n\n const keysA = Object.keys(objA)\n if (keysA.length !== Object.keys(objB).length) {\n return false\n }\n\n for (const key of keysA) {\n if (\n !Object.prototype.hasOwnProperty.call(objB, key) ||\n !Object.is(objA[key as keyof T], objB[key as keyof T])\n ) {\n return false\n }\n }\n return true\n}\n"],"names":["useSyncExternalStoreWithSelector","collectionsStore","Collection"],"mappings":"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"}