UNPKG

react-granular-store

Version:

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

140 lines 5.49 kB
// index.ts import { useCallback, useEffect, useState } from "react"; var defaultOptions = { equalityFn: (oldValue, newValue) => oldValue === newValue, batchUpdates: false }; var Store = class { // The state is public so that it can be accessed directly if needed. Not recommended. state; options; // Callbacks are stored as keys in an object to get full inference support. Initially a map was used, but it's difficult // to use generics to link the key of the map to the value being returned from the map when you use the getter. callbacks = {}; // Deferred state is used to batch updates. When setState is called, the state is not updated immediately, but instead // stored in the deferredState map. When the batch is resolved, the deferredState is cycled through and the state is // updated. _deferredState = /* @__PURE__ */ new Map(); // This flag is used to prevent multiple setTimeouts from being set when a batch update is already pending. _awaitingUpdate = false; // Using the generic as the type of defaultValues is the magic that allows the state to be inferred correctly. This is // overridden by providing the generic directly when instantiating. constructor(defaultValues, options) { this.state = defaultValues; this.options = { ...defaultOptions, ...options }; } // Get the state for a key. If running in batch mode (default), the accurate state will be in the deferredState map. getState(key) { return this._deferredState.has(key) ? this._deferredState.get(key) : this.state[key]; } // Set the state for a key. If running in batch mode (default), the state is not updated immediately, but stored in // the deferredState map. When the batch is resolved, the deferredState is cycled through and the state is updated. setState(key, newValue) { const resolvedValue = this._resolveNewValue(key, newValue); if (this.options.batchUpdates) { this._deferredState.set(key, resolvedValue); this._flagDeferredStateForResolution(); } else { this._setState(key, resolvedValue); } } // Low level (but public) function to register a callback for a key on(key, callback) { const existingCallbacks = this.callbacks[key]; if (existingCallbacks) { existingCallbacks.add(callback); } else { this.callbacks[key] = /* @__PURE__ */ new Set([callback]); } } // Low level (but public) function to remove a callback for a key. Note: callbacks are not removed unless they are // an exact reference match. off(key, callback) { const existingCallbacks = this.callbacks[key]; if (existingCallbacks) { existingCallbacks.delete(callback); } } // Subscribe to a key. This function returns a function that can be called to unsubscribe the callback. subscribe(key, callback) { this.on(key, callback); return () => this.off(key, callback); } // Set the main internal state. This is the core function that sets the state and triggers callbacks. This is also where // the equality function is used to determine if the state has changed. _setState(key, newValue) { const oldValue = this.state[key]; if (this.options.equalityFn(oldValue, newValue, key)) { return; } this.state[key] = newValue; const existingCallbacks = this.callbacks[key]; if (existingCallbacks) { existingCallbacks.forEach((callback) => callback(newValue)); } } // This function is to determine the new value of the state given the SetStateArgument, which could be a function. If it's a // function, it's called with the previous value, which needs to potentially come from deferred state if in batch mode (default). _resolveNewValue(key, newValue) { if (typeof newValue === "function") { const prevValue = this.getState(key); return newValue(prevValue); } return newValue; } // This function is used to batch updates. It sets a timeout to resolve the deferred state in the next tick. If a // batch is already pending, it does nothing. _flagDeferredStateForResolution = () => { if (this._awaitingUpdate) return; this._awaitingUpdate = true; setTimeout(() => { this._resolveDeferredState(); this._awaitingUpdate = false; }, 0); }; // This function is used to resolve the deferred state at the end of the tick in batch mode. It cycles through the // deferredState entries and sets the state. _resolveDeferredState() { this._deferredState.forEach((newValue, key) => { this._setState(key, newValue); }); this._deferredState.clear(); } }; function useStoreValue(store, key) { const [state, setState] = useState(() => store?.getState(key) ?? null); useEffect(() => { if (!store) { setState(() => null); return; } setState(() => store.getState(key)); const unsubscribe = store.subscribe(key, (next) => setState(() => next)); return () => unsubscribe(); }, [store, key]); return state; } function useStoreUpdate(store, key) { return useCallback( (newValue) => { if (!store) return; store.setState(key, newValue); }, [store, key] ); } function useStoreState(store, key) { const state = useStoreValue(store, key); const updateState = useStoreUpdate(store, key); return [state, updateState]; } var RecordStore = class extends Store { }; export { RecordStore, Store as default, useStoreState, useStoreUpdate, useStoreValue }; //# sourceMappingURL=index.mjs.map