import { createStore, zeduxTypes, Store } from '@zedux/core' import { createInjector } from '../factories/createInjector' import { InjectStoreConfig, PartialAtomInstance } from '../types/index' import { InjectorDescriptor, prefix } from '../utils/index' export const doSubscribe = ( instance: PartialAtomInstance, store: Store ) => store.subscribe((newState, oldState, action) => { // Nothing to do if the state hasn't changed. Also, ignore state updates // during evaluation. TODO: Create an ecosystem-level flag to turn on // warning logging for state-updates-during-evaluation, since this may be // considered an anti-pattern. if (instance._isEvaluating || action.meta === zeduxTypes.ignore) { return } const isBatch = action?.meta === zeduxTypes.batch instance._scheduleEvaluation( { newState, oldState, operation: 'injectStore', reasons: [ { action, newState, oldState, operation: 'dispatch', sourceType: 'Store', type: 'state changed', }, ], sourceType: 'Injector', type: 'state changed', }, isBatch ) // run the scheduler synchronously after every store update unless batching if (!isBatch) { instance.ecosystem._scheduler.flush() } }) /** * injectStore() * * A convenience utility for quickly creating and optionally subscribing to * stores in atoms. * * The returned store is a stable reference - it will not change on subsequent * evaluations. It can therefore be returned from the instance factory as the * instance's store. It also doesn't _need_ to be added to injector deps arrays * (though there's no harm in doing so). * * Accepts either the initial store state or a function that returns the store. * Use the latter for maximum flexibility. * * Subscribes to the store by default, causing the atom to be reevaluated on * every state change. This can be changed by passing `subscribe: false`. * * In most cases you won't need to prevent subscribing. But it can be a useful * performance optimization. * * ```ts * import { atom, injectStore } from '@zedux/atoms' * * const inputAtom = atom('input', () => { * const store = injectStore('', { subscribe: false }) * * return store * }) * ``` * * When `hydrate: true` is passed, the store's initial state will be set to the * value from the last call to `ecosystem.hydrate()` whose key matches this atom * instance. The hydrated value will be passed to the atom's `hydrate` config * option, if any, to transform the value first. * * When the function `storeFactory` overload is used and `hydrate: true` is * passed, the transformed hydration will be passed to the store factory * function and it's up to you to use it to hydrate the store you create. * * ```ts * const store = injectStore( * hydration => createStore(null, hydration ?? defaultVal), * { hydrate: true } * ) * // or simply: * const store = injectStore(defaultVal, { hydrate: true }) * ``` * * @param storeFactory - Either a function that returns a store or the initial * state of the store * @param config - A config object. Accepts the following properties: * - `hydrate` - Whether to try hydrating this store with * - `subscribe` - Whether to subscribe to the store (default: `true`) * @returns Store */ export const injectStore: { ( storeFactory: State | ((hydration?: State) => Store), config?: InjectStoreConfig ): Store (): Store } = createInjector( 'injectStore', ( instance: PartialAtomInstance, storeFactory?: State | ((hydration?: State) => Store), config?: InjectStoreConfig ) => { const subscribe = config?.subscribe ?? true const getStore = typeof storeFactory === 'function' ? (storeFactory as () => Store) : (hydration?: State) => createStore(null, hydration ?? storeFactory) const store = getStore( config?.hydrate ? instance.ecosystem.hydration?.[instance.id] : undefined ) const subscription = subscribe && doSubscribe(instance, store) return { cleanup: subscription ? () => subscription.unsubscribe() : undefined, result: store, type: `${prefix}/store`, } as InjectorDescriptor> }, ( prevDescriptor: InjectorDescriptor>, instance: PartialAtomInstance, storeFactory?: State | ((hydration?: State) => Store), config?: InjectStoreConfig ) => { const subscribe = config?.subscribe ?? true const prevsubscribe = !!prevDescriptor.cleanup if (prevsubscribe === subscribe) return prevDescriptor // we were subscribed, now we're not if (!subscribe) { // cleanup must be defined here. This cast is fine: ;(prevDescriptor.cleanup as () => void)() prevDescriptor.cleanup = undefined return prevDescriptor } // we weren't subscribed, now we are const subscription = doSubscribe(instance, prevDescriptor.result) prevDescriptor.cleanup = () => subscription.unsubscribe() return prevDescriptor } )