react-granular-store
Version:
Granular react store for subscribing to specific parts of a state tree
140 lines • 5.49 kB
JavaScript
// 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