UNPKG

contextors

Version:

A library for creating memoised "context selector" functions

230 lines (225 loc) 7.8 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); // src/index.ts import { isContext as isContext2, useSubscriber } from "contexto"; import { useEffect, useReducer, useRef, useState } from "react"; // src/memoslots.ts var MemoSlotProvider = class { constructor() { this.slots = []; } iterator() { let i = 0; const { slots } = this; return { next() { if (i >= slots.length) slots.push({ sourceValues: [], tag: void 0, out: void 0 }); return slots[i++]; } }; } }; // src/rawcontextor.ts import { isContext } from "contexto"; var RawContextor = class { constructor(sources, combiner, isEqual) { this.sources = sources; this.combiner = combiner; this.isEqual = isEqual; this.multiCache = /* @__PURE__ */ new WeakMap(); // Arbitrary object scoped to the Contextor that can be used to index a WeakMap this.TerminalCacheKey = {}; this.contexts = /* @__PURE__ */ new Set(); for (const source of sources) { if (isContext(source)) this.contexts.add(source); else source.contexts.forEach((context) => this.contexts.add(context)); } } subscribe(subscriber, onChange, tag, opts) { const { sources } = this; const unsubscribers = []; const unsubscribeAll = () => unsubscribers.forEach((unsubscribe) => unsubscribe()); const sourceValues = sources.map( (source, i) => { const updateValue = (newValue) => { sourceValues[i] = newValue; onChange(this.computeWithCache(sourceValues, tag, opts == null ? void 0 : opts.memoProvider.iterator())); }; const [initialValue2, unsubscribe] = isContext(source) ? subscriber(source, updateValue) : source.subscribe(subscriber, updateValue, tag, opts); unsubscribers.push(unsubscribe); return initialValue2; } ); const initialValue = this.computeWithCache(sourceValues, tag, opts == null ? void 0 : opts.memoProvider.iterator()); return [initialValue, unsubscribeAll]; } computeWithCache(sourceValues, tag, memoSlots) { return this.isEqual ? this.computeWithMemoiseCache(sourceValues, tag, memoSlots) : this.computeWithMultiCache(sourceValues, tag); } computeWithMemoiseCache(sourceValues, tag, memoSlots) { const memoSlot = memoSlots == null ? void 0 : memoSlots.next(); if ((memoSlot == null ? void 0 : memoSlot.sourceValues) && this.isEqual( // eslint-disable-line @typescript-eslint/no-non-null-assertion [sourceValues, tag], [memoSlot.sourceValues, memoSlot.tag] )) return memoSlot.out; const out = this.combiner(...sourceValues, tag); if (memoSlot) { memoSlot.sourceValues = sourceValues; memoSlot.tag = tag; memoSlot.out = out; } return out; } computeWithMultiCache(sourceValues, tag) { let cacheRef = this.multiCache; const sourceValuesWithTag = [...sourceValues, tag]; for (const source of sourceValuesWithTag) { if (isObject(source)) { let nextCacheRef = cacheRef.get(source); if (nextCacheRef) cacheRef = nextCacheRef; else { nextCacheRef = /* @__PURE__ */ new WeakMap(); cacheRef.set(source, nextCacheRef); cacheRef = nextCacheRef; } } } const terminalCache = cacheRef.get(this.TerminalCacheKey); if (isTerminalCache(terminalCache) && shallowEqual(terminalCache.keys, sourceValuesWithTag)) { return terminalCache.value; } const value = this.combiner(...sourceValues, tag); cacheRef.set(this.TerminalCacheKey, { keys: sourceValuesWithTag, value }); return value; } }; function isContextor(value) { return value instanceof RawContextor; } function isTerminalCache(cache) { return cache !== void 0 && !(cache instanceof WeakMap); } function isObject(value) { return value instanceof Object; } var shallowEqual = (array1, array2) => array1 === array2 || array1.length === array2.length && array1.every((keyComponent, i) => keyComponent === array2[i]); // src/index.ts function createContextor(...params) { const rawParams = [...params]; const lastParam = rawParams.pop(); const [combiner, options] = typeof lastParam === "object" ? [rawParams.pop(), lastParam] : [lastParam, {}]; const sources = Array.isArray(rawParams[0]) ? rawParams[0] : rawParams; assertValidSources(sources); return new RawContextor(sources, combiner, options == null ? void 0 : options.isEqual); } function assertValidSources(sources) { if (!sources.every((source) => isContext2(source) || isContextor(source))) { if (sources.some(isReactContext)) { throw new Error( "createContextor received React.Context source, but Contexto.Context source is required" ); } const sourceTypes = sources.map( (source) => typeof source === "function" ? source.toString() : typeof source ).join(", "); throw new Error( `createContextor sources must be Context or Contextor, but received the following types: [${sourceTypes}]` ); } } function isReactContext(value) { return value !== null && typeof value === "object" && "$$typeof" in value && value.$$typeof === Symbol.for("react.context"); } function contextorReducer(state, action) { const { value, unsubscribe, subscribe } = state; switch (action.type) { case "setValue": return ( // Only update state if value has changed action.value !== value ? { value: action.value, unsubscribe, subscribe } : state ); case "unsetContextor": unsubscribe == null ? void 0 : unsubscribe(); return { value, subscribe }; case "setContextor": return __spreadProps(__spreadValues({}, subscribe(action.contextor, action.tag)), { subscribe }); default: return state; } } function useContextor(contextor, tag) { const subscriber = useSubscriber(); const [memoProvider] = useState(() => new MemoSlotProvider()); const subscribe = (newContextor) => { const [initialValue, unsubscribe] = newContextor.subscribe( subscriber, (updatedValue) => dispatch({ type: "setValue", value: updatedValue }), tag, // nb: tag may be undefined here but only if Tag extends undefined { memoProvider } ); return { value: initialValue, unsubscribe, subscribe }; }; const [{ value: currentValue }, dispatch] = useReducer( contextorReducer, contextor, subscribe ); useEffectOnUpdate( () => { dispatch({ type: "setContextor", contextor, tag }); return () => dispatch({ type: "unsetContextor" }); }, [dispatch, contextor, tag] ); return currentValue; } function useEffectOnUpdate(effect, deps) { const hasMounted = useRef(false); useEffect( () => { if (hasMounted.current) return effect(); hasMounted.current = true; }, [hasMounted, ...deps] // eslint-disable-line react-hooks/exhaustive-deps ); } export { createContextor, useContextor };