UNPKG

react-granular-store

Version:

Granular react store for subscribing to specific parts of a state tree

1 lines 14.3 kB
{"version":3,"sources":["../index.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\n\n// Why any? Unknown is not appropriate here because we dont want to have to determine the type of the state when we access it.\n// By using any, we can quietly cast the state to the correct type when we access it. The type is still inferred correctly,\n// but we need this value in the state tree to be completely unconstrained.\nexport type StateTree = Record<string | number | symbol, any>;\n\nexport interface StoreOptions<State extends StateTree> {\n\t// Let the consumer custom configure equality checks\n\tequalityFn?: <Key extends keyof State>(oldValue: State[Key], newValue: State[Key], key: Key) => boolean;\n\t// Run callbacks to state updates synchronously or batch them\n\tbatchUpdates?: boolean;\n}\n\nconst defaultOptions: Required<StoreOptions<StateTree>> = {\n\tequalityFn: (oldValue, newValue) => oldValue === newValue,\n\tbatchUpdates: false,\n};\n\n// Equivalent to React's SetStateAction\nexport type SetStateArgument<T> = T | ((prev: T) => T);\n\n// Main store class\nexport default class Store<State extends StateTree> {\n\t// The state is public so that it can be accessed directly if needed. Not recommended.\n\tpublic state: State;\n\tpublic options: Required<StoreOptions<State>>;\n\t// Callbacks are stored as keys in an object to get full inference support. Initially a map was used, but it's difficult\n\t// to use generics to link the key of the map to the value being returned from the map when you use the getter.\n\tpublic callbacks: {\n\t\t[key in keyof State]?: Set<(newValue: State[key]) => void>;\n\t} = {};\n\n\t// Deferred state is used to batch updates. When setState is called, the state is not updated immediately, but instead\n\t// stored in the deferredState map. When the batch is resolved, the deferredState is cycled through and the state is\n\t// updated.\n\tprivate _deferredState: Map<keyof State, State[keyof State]> = new Map();\n\t// This flag is used to prevent multiple setTimeouts from being set when a batch update is already pending.\n\tprivate _awaitingUpdate = false;\n\n\t// Using the generic as the type of defaultValues is the magic that allows the state to be inferred correctly. This is\n\t// overridden by providing the generic directly when instantiating.\n\tconstructor(defaultValues: State, options?: StoreOptions<State>) {\n\t\tthis.state = defaultValues;\n\t\t// Let the default options be overridden by the provided options\n\t\tthis.options = { ...defaultOptions, ...options };\n\t}\n\n\t// Get the state for a key. If running in batch mode (default), the accurate state will be in the deferredState map.\n\tpublic getState<Key extends keyof State>(key: Key) {\n\t\treturn this._deferredState.has(key) ? (this._deferredState.get(key) as State[Key]) : this.state[key];\n\t}\n\n\t// Set the state for a key. If running in batch mode (default), the state is not updated immediately, but stored in\n\t// the deferredState map. When the batch is resolved, the deferredState is cycled through and the state is updated.\n\tpublic setState<Key extends keyof State>(key: Key, newValue: SetStateArgument<State[Key]>) {\n\t\tconst resolvedValue = this._resolveNewValue(key, newValue);\n\t\tif (this.options.batchUpdates) {\n\t\t\tthis._deferredState.set(key, resolvedValue);\n\t\t\tthis._flagDeferredStateForResolution();\n\t\t} else {\n\t\t\tthis._setState(key, resolvedValue);\n\t\t}\n\t}\n\n\t// Low level (but public) function to register a callback for a key\n\tpublic on<Key extends keyof State>(key: Key, callback: (newValue: State[Key]) => void) {\n\t\tconst existingCallbacks = this.callbacks[key];\n\t\tif (existingCallbacks) {\n\t\t\texistingCallbacks.add(callback);\n\t\t} else {\n\t\t\tthis.callbacks[key] = new Set([callback]);\n\t\t}\n\t}\n\n\t// Low level (but public) function to remove a callback for a key. Note: callbacks are not removed unless they are\n\t// an exact reference match.\n\tpublic off<Key extends keyof State>(key: Key, callback: (newValue: State[Key]) => void) {\n\t\tconst existingCallbacks = this.callbacks[key];\n\t\tif (existingCallbacks) {\n\t\t\texistingCallbacks.delete(callback);\n\t\t}\n\t}\n\n\t// Subscribe to a key. This function returns a function that can be called to unsubscribe the callback.\n\tpublic subscribe<Key extends keyof State>(key: Key, callback: (newValue: State[Key]) => void) {\n\t\tthis.on(key, callback);\n\t\treturn () => this.off(key, callback);\n\t}\n\n\t// Set the main internal state. This is the core function that sets the state and triggers callbacks. This is also where\n\t// the equality function is used to determine if the state has changed.\n\tprivate _setState<Key extends keyof State>(key: Key, newValue: State[Key]) {\n\t\t// determine equality and skip if equal\n\t\tconst oldValue = this.state[key];\n\t\tif (this.options.equalityFn(oldValue, newValue, key)) {\n\t\t\treturn;\n\t\t}\n\t\t// All these checks before finally setting the state here\n\t\tthis.state[key] = newValue;\n\t\t// Call all the callbacks for this key\n\t\tconst existingCallbacks = this.callbacks[key];\n\t\tif (existingCallbacks) {\n\t\t\texistingCallbacks.forEach((callback) => callback(newValue));\n\t\t}\n\t}\n\n\t// This function is to determine the new value of the state given the SetStateArgument, which could be a function. If it's a\n\t// function, it's called with the previous value, which needs to potentially come from deferred state if in batch mode (default).\n\tprivate _resolveNewValue<Key extends keyof State>(key: Key, newValue: SetStateArgument<State[Key]>) {\n\t\tif (typeof newValue === 'function') {\n\t\t\tconst prevValue = this.getState(key);\n\t\t\t// newValue has to be cast to a function because there's no way of knowing if the actual state type is a function.\n\t\t\t// This is a limitation that also exists in React useState. If you need to set a function as state, you have to wrap\n\t\t\t// it in a function that returns it.\n\t\t\treturn (newValue as (prev: State[Key]) => State[Key])(prevValue);\n\t\t}\n\t\treturn newValue;\n\t}\n\n\t// This function is used to batch updates. It sets a timeout to resolve the deferred state in the next tick. If a\n\t// batch is already pending, it does nothing.\n\tprivate _flagDeferredStateForResolution = () => {\n\t\tif (this._awaitingUpdate) return;\n\t\tthis._awaitingUpdate = true;\n\t\tsetTimeout(() => {\n\t\t\tthis._resolveDeferredState();\n\t\t\tthis._awaitingUpdate = false;\n\t\t}, 0);\n\t};\n\n\t// This function is used to resolve the deferred state at the end of the tick in batch mode. It cycles through the\n\t// deferredState entries and sets the state.\n\tprivate _resolveDeferredState() {\n\t\t// Cycle through the deferredState entries and set the state\n\t\tthis._deferredState.forEach(<Key extends keyof State>(newValue: State[Key], key: Key) => {\n\t\t\tthis._setState(key, newValue);\n\t\t});\n\t\tthis._deferredState.clear();\n\t}\n}\n\n// This hook subscribes to a store and syncs the state with a react useState hook. It returns the current state and\n// unsubscribes when the component unmounts. There is an overload to return alternate types if the store is possibly\n// null. This is useful for when the store is being passed in as a prop or through a context.\nexport function useStoreValue<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State>,\n\tkey: Key,\n): State[Key];\nexport function useStoreValue<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State> | null,\n\tkey: Key,\n): State[Key] | null;\nexport function useStoreValue<State extends StateTree, Key extends keyof State>(store: Store<State> | null, key: Key) {\n\t// Use lazy initializer so a function value is stored as-is, not treated as an initializer\n\tconst [state, setState] = useState(() => store?.getState(key) ?? null);\n\n\tuseEffect(() => {\n\t\tif (!store) {\n\t\t\tsetState(() => null);\n\t\t\treturn;\n\t\t}\n\t\t// Set the initial state (wrap so function values are not treated as updaters)\n\t\tsetState(() => store.getState(key));\n\t\t// Subscribe to the store for updates\n\t\tconst unsubscribe = store.subscribe(key, (next) => setState(() => next));\n\t\treturn () => unsubscribe();\n\t}, [store, key]);\n\n\treturn state;\n}\n\n// This hook subscribes to a store and returns a function that can be called to update the state. This hook does not\n// react to the state changing, and so is useful for a component that needs to alter state but doesn't read it. There is\n// also an overload to return alternate types if the store is possibly null. This is useful for when the store is being\n// passed in as a prop or through a context.\nexport function useStoreUpdate<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State>,\n\tkey: Key,\n): (newValue: SetStateArgument<State[Key]>) => void;\nexport function useStoreUpdate<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State> | null,\n\tkey: Key,\n): (newValue: SetStateArgument<State[Key]>) => void;\nexport function useStoreUpdate<State extends StateTree, Key extends keyof State>(store: Store<State> | null, key: Key) {\n\treturn useCallback(\n\t\t(newValue: SetStateArgument<State[Key]>) => {\n\t\t\tif (!store) return;\n\t\t\tstore.setState(key, newValue);\n\t\t},\n\t\t[store, key],\n\t);\n}\n\n// This hook combines the useStoreValue and useStoreUpdate hooks to return the current state and a function to update the\n// state. This is useful for components that need to read and update the state. There is also an overload to return\n// alternate types if the store is possibly null. This is useful for when the store is being passed in as a prop or\n// through a context.\nexport function useStoreState<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State>,\n\tkey: Key,\n): [State[Key], (newValue: SetStateArgument<State[Key]>) => void];\nexport function useStoreState<State extends StateTree, Key extends keyof State>(\n\tstore: Store<State> | null,\n\tkey: Key,\n): [State[Key] | null, (newValue: SetStateArgument<State[Key]>) => void];\nexport function useStoreState<State extends StateTree, Key extends keyof State>(store: Store<State> | null, key: Key) {\n\tconst state = useStoreValue(store, key);\n\tconst updateState = useStoreUpdate(store, key);\n\n\treturn [state, updateState] as const;\n}\n\n// Record stores are a special case of stores where the value of each key is of the same type. Additionally, you can\n// try to access keys that are not explicitly specified because any regular key will match this record type. The only\n// caveat of this approach is that all keys, including ones that definitely exist, return the value as possibly\n// undefined.\n\n// An example use case for a RecordStore is a list of items where the key is the id of the item. You can access any\n// item by id, and if the item doesn't exist, you get undefined. Components can now subscribe to the ID of something\n// and get the item, even if it doesn't exist yet. And when it does exist, the component will update.\n\n// You can think of a RecordStore like a Map, but with the ability to subscribe to keys and get updates when they change.\n\n// The state of the RecordStore is similar to the state tree, but because we know the value types are all the same, we\n// can use a generic to provide the value rather than using any.\nexport interface RecordStoreState<T> {\n\t[key: string | number | symbol]: T | undefined;\n}\n\n// A RecordStore is nothing but an extended Store with a specific type for the state tree. The generic type can be\n// inferred from the default value provided when instantiating the store.\nexport class RecordStore<T> extends Store<RecordStoreState<T>> {}\n"],"mappings":"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","names":[]}