import { createInjector } from '../factories/createInjector' import { EffectCallback, InjectorDeps } from '../types/index' import { haveDepsChanged, InjectorDescriptor, prefix } from '../utils/index' interface EffectInjectorDescriptor extends InjectorDescriptor { deps: InjectorDeps } const getTask = ( effect: EffectCallback, descriptor: EffectInjectorDescriptor ) => { const task = () => { const cleanup = effect() // now that the task has run, there's no need for the scheduler cleanup // function; replace it with the cleanup logic returned from the effect // (if any). If a promise was returned, ignore it. descriptor.cleanup = typeof cleanup === 'function' ? cleanup : undefined } return task } /** * Runs a deferred side effect. This is just like React's `useEffect`. When * `deps` change on a subsequent reevaluation, the previous effect will be * cleaned up and the effect will rerun. * * Return a cleanup function to clean up resources when the effect reruns or the * current atom instance is destroyed. * * Unlike `useEffect`, you can return a promise from `injectEffect` (e.g. by * passing an async function). This is only for convenience in cases where you * don't have anything to cleanup, as you'll be unable to clean up resources if * you return a promise. */ export const injectEffect = createInjector( 'injectEffect', ( instance, effect: EffectCallback, deps?: InjectorDeps, config?: { synchronous?: boolean } ) => { const descriptor: EffectInjectorDescriptor = { deps, type: `${prefix}/effect`, } if (!instance.ecosystem.ssr) { const task = getTask(effect, descriptor) descriptor.cleanup = () => { instance.ecosystem._scheduler.unschedule(task) descriptor.cleanup = undefined } if (config?.synchronous) { task() } else { instance.ecosystem._scheduler.schedule({ task, type: 4, // RunEffect (4) }) } } return descriptor }, ( prevDescriptor, instance, effect: EffectCallback, deps?: InjectorDeps, config?: { synchronous?: boolean } ) => { if (instance.ecosystem.ssr) return prevDescriptor const depsHaveChanged = haveDepsChanged(prevDescriptor?.deps, deps) if (!depsHaveChanged) return prevDescriptor prevDescriptor.cleanup?.() const task = getTask(effect, prevDescriptor) // this cleanup should be unnecessary since effects run immediately every // time except init. Leave this though in case we add a way to update an // atom instance without flushing the scheduler prevDescriptor.cleanup = () => { instance.ecosystem._scheduler.unschedule(task) prevDescriptor.cleanup = undefined } prevDescriptor.deps = deps if (config?.synchronous) { task() } else { instance.ecosystem._scheduler.schedule({ task, type: 4, // RunEffect (4) }) } return prevDescriptor } )