pinia-react
Version:
Intuitive, type safe and flexible Store for React
395 lines (393 loc) • 14.4 kB
JavaScript
// src/rootStore.ts
var activePinia;
function setActivePinia(pinia) {
activePinia = pinia;
}
function getActivePinia() {
if (!activePinia) {
throw new Error(
"[pinia-react] getActivePinia was called with no active Pinia. Did you forget to install pinia?\nconst pinia = createPinia() or createPinia() first.\n"
);
}
return activePinia;
}
// src/createPinia.ts
function createPinia() {
const state = {};
const _p = [];
const _s = /* @__PURE__ */ new Map();
const _scopes = /* @__PURE__ */ new Map();
const pinia = {
use(plugin) {
_p.push(plugin);
return this;
},
_p,
_s,
_scopes,
state
};
setActivePinia(pinia);
return pinia;
}
// src/store.ts
import { enablePatches, produce, setAutoFreeze } from "immer";
import { useCallback, useRef, useSyncExternalStore } from "react";
enablePatches();
setAutoFreeze(false);
var activeListenerId = null;
var activeGetterKey = null;
function isAffected(patches, trackedPaths) {
if (trackedPaths.size === 0) return false;
const tracked = Array.from(trackedPaths).map((p) => p.split("."));
for (const patch of patches) {
const patchPath = patch.path.map(String);
for (const trackedPath of tracked) {
const len = Math.min(patchPath.length, trackedPath.length);
let isPrefixMatch = true;
for (let i = 0; i < len; i++) {
if (patchPath[i] !== trackedPath[i]) {
isPrefixMatch = false;
break;
}
}
if (isPrefixMatch) return true;
}
}
return false;
}
function defineStore(id, options) {
const getters = options.getters || {};
function resolveGetterDependencies(getterName, getterDepsMap, visited = /* @__PURE__ */ new Set()) {
if (visited.has(getterName)) {
console.warn(`[pinia-react] Circular dependency in getters detected involving: ${getterName}`);
return /* @__PURE__ */ new Set();
}
visited.add(getterName);
const finalDeps = /* @__PURE__ */ new Set();
const directDeps = getterDepsMap.get(getterName);
if (!directDeps) return finalDeps;
for (const dep of directDeps) {
if (dep in getters) {
const nestedDeps = resolveGetterDependencies(dep, getterDepsMap, visited);
nestedDeps.forEach((d) => finalDeps.add(d));
} else {
finalDeps.add(dep);
}
}
return finalDeps;
}
function createStoreInstance() {
const pinia = getActivePinia();
const initialState = options.state();
let storePublicApi;
let devTools;
let isTimeTraveling = false;
const localScope = {
currentState: initialState,
listeners: /* @__PURE__ */ new Set(),
getterCache: /* @__PURE__ */ new Map(),
getterDependencies: /* @__PURE__ */ new Map(),
subscribers: /* @__PURE__ */ new Map(),
createStoreProxy: (_onAccess) => storePublicApi
};
pinia._scopes.set(id, localScope);
const isGetterComputing = /* @__PURE__ */ new Set();
const emit = (nextState, oldState, patches) => {
localScope.listeners.forEach((fn) => fn(nextState, oldState, patches));
localScope.subscribers.forEach((getterKeys, storeId) => {
const subscriberScope = pinia._scopes.get(storeId);
if (!subscriberScope) return;
let shouldNotify = false;
getterKeys.forEach((key) => {
if (subscriberScope.getterCache.has(key)) {
subscriberScope.getterCache.delete(key);
shouldNotify = true;
}
});
if (shouldNotify) {
const oldSubState = subscriberScope.currentState;
const newSubState = { ...oldSubState };
subscriberScope.currentState = newSubState;
pinia.state[storeId] = newSubState;
subscriberScope.listeners.forEach((fn) => fn(newSubState, oldSubState, []));
}
});
};
const internalPatch = (updater, actionName, isReset = false) => {
if (isTimeTraveling) return;
const oldState = localScope.currentState;
let patches = [];
const nextState = produce(oldState, updater, (p) => {
patches = p;
});
if (patches.length > 0 || isReset) {
localScope.currentState = nextState;
pinia.state[id] = nextState;
if (devTools) {
devTools.send({ type: actionName, payload: patches }, nextState);
}
emit(nextState, oldState, patches);
}
};
const $patch = (updater) => {
internalPatch((draft) => {
updater(draft);
}, "@patch");
};
const $reset = () => internalPatch(() => options.state(), "@reset", true);
const $subscribe = (callback) => {
const listener = (state, prev, patches) => callback(state, prev);
localScope.listeners.add(listener);
return () => localScope.listeners.delete(listener);
};
const getterInvalidationListener = (_state, _prevState, patches) => {
localScope.getterDependencies.forEach((_deps, getterName) => {
const resolvedDeps = resolveGetterDependencies(getterName, localScope.getterDependencies);
if (isAffected(patches, resolvedDeps)) {
localScope.getterCache.delete(getterName);
}
});
};
localScope.listeners.add(getterInvalidationListener);
const originalActions = options.actions || {};
const wrappedActions = {};
const proxyTarget = {};
function createStoreProxy(onAccess) {
const readonlyWarning = () => {
console.warn(`[${id}] Store is read-only. Use actions for mutations.`);
return false;
};
const createStateProxy = (stateTarget, path, onDeepAccess) => {
return new Proxy(stateTarget, {
get(obj, key) {
if (typeof key === "symbol") return Reflect.get(obj, key);
const currentPath = [...path, String(key)];
const value = Reflect.get(obj, key);
if (typeof value === "object" && value !== null) {
return createStateProxy(value, currentPath, onDeepAccess);
}
onDeepAccess?.(currentPath);
return value;
},
set: readonlyWarning
});
};
return new Proxy(proxyTarget, {
get(_target, key, receiver) {
const strKey = String(key);
if (strKey === "$state") return localScope.currentState;
if (strKey === "$patch") return $patch;
if (strKey === "$reset") return $reset;
if (strKey === "$subscribe") return $subscribe;
const state = localScope.currentState;
if (strKey in state) {
const value = state[strKey];
if (typeof value === "object" && value !== null) {
return createStateProxy(value, [strKey], onAccess);
}
onAccess?.([strKey]);
return value;
}
if (strKey in getters) {
onAccess?.([strKey]);
if (localScope.getterCache.has(strKey)) return localScope.getterCache.get(strKey);
if (isGetterComputing.has(strKey)) {
console.warn(`[pinia-react] Circular dependency detected in getter "${strKey}"`);
return void 0;
}
isGetterComputing.add(strKey);
const dependencies = /* @__PURE__ */ new Set();
const prevListenerId = activeListenerId;
const prevGetterKey = activeGetterKey;
activeListenerId = id;
activeGetterKey = strKey;
try {
const onGetterAccess = (path) => {
dependencies.add(path[0]);
};
const trackingProxyForThis = createStoreProxy(onGetterAccess);
const trackingStateProxy = createStateProxy(state, [], onGetterAccess);
const result = getters[strKey].call(trackingProxyForThis, trackingStateProxy);
localScope.getterDependencies.set(strKey, dependencies);
localScope.getterCache.set(strKey, result);
return result;
} finally {
activeListenerId = prevListenerId;
activeGetterKey = prevGetterKey;
isGetterComputing.delete(strKey);
}
}
if (strKey in wrappedActions) {
return wrappedActions[strKey];
}
return Reflect.get(_target, key, receiver);
},
set(_target, key, value, receiver) {
const strKey = String(key);
if (strKey === "$state") {
console.warn(`[${id}] Do not replace "$state" directly. Use "$patch()" to replace the whole state.`);
return false;
}
if (strKey in localScope.currentState || strKey in getters || strKey in wrappedActions) {
return readonlyWarning();
}
return Reflect.set(_target, key, value, receiver);
}
});
}
storePublicApi = createStoreProxy();
Object.keys(originalActions).forEach((actionName) => {
const originalAction = originalActions[actionName];
wrappedActions[actionName] = (...args) => {
let returnValue;
const recipe = (draft) => {
const actionContextProxy = new Proxy({}, {
get(_, key) {
const strKey = String(key);
if (Reflect.has(draft, strKey)) return draft[strKey];
if (strKey in getters) {
return getters[strKey].call(actionContextProxy, draft);
}
return Reflect.get(storePublicApi, key, storePublicApi);
},
set(_, key, value) {
;
draft[String(key)] = value;
return true;
}
});
returnValue = originalAction.apply(actionContextProxy, args);
};
internalPatch(recipe, actionName);
return returnValue;
};
});
localScope.createStoreProxy = createStoreProxy;
pinia._p.forEach((plugin) => {
const pluginResult = plugin({ id, store: storePublicApi, options });
if (pluginResult) {
Object.defineProperties(proxyTarget, Object.getOwnPropertyDescriptors(pluginResult));
}
});
pinia._s.set(id, storePublicApi);
if (typeof window !== "undefined" && window.__REDUX_DEVTOOLS_EXTENSION__) {
devTools = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name: id });
devTools.init(localScope.currentState);
devTools.subscribe((message) => {
if (message.type === "DISPATCH") {
const payloadType = message.payload?.type;
switch (payloadType) {
case "JUMP_TO_STATE":
case "JUMP_TO_ACTION":
case "IMPORT_STATE": {
const newState = typeof message.state === "string" ? JSON.parse(message.state) : message.state;
if (!newState || typeof newState !== "object") return;
isTimeTraveling = true;
const oldState = localScope.currentState;
localScope.currentState = newState;
pinia.state[id] = newState;
localScope.getterCache.clear();
emit(newState, oldState, []);
isTimeTraveling = false;
break;
}
case "COMMIT": {
devTools.init(localScope.currentState);
break;
}
case "ROLLBACK": {
const newState = typeof message.state === "string" ? JSON.parse(message.state) : message.state;
if (!newState || typeof newState !== "object") return;
isTimeTraveling = true;
const oldState = localScope.currentState;
localScope.currentState = newState;
pinia.state[id] = newState;
localScope.getterCache.clear();
emit(newState, oldState, []);
isTimeTraveling = false;
break;
}
case "RESET": {
const originalState = options.state();
devTools.init(originalState);
internalPatch(() => originalState, "@reset", true);
break;
}
default:
break;
}
}
});
}
return storePublicApi;
}
function getStore() {
const pinia = getActivePinia();
if (!pinia._s.has(id)) {
createStoreInstance();
}
if (activeListenerId && activeGetterKey && activeListenerId !== id) {
const accessedStoreScope = pinia._scopes.get(id);
if (accessedStoreScope) {
let subscribers = accessedStoreScope.subscribers.get(activeListenerId);
if (!subscribers) {
subscribers = /* @__PURE__ */ new Set();
accessedStoreScope.subscribers.set(activeListenerId, subscribers);
}
subscribers.add(activeGetterKey);
}
}
return pinia._s.get(id);
}
function useStore() {
const pinia = getActivePinia();
if (!pinia._s.has(id)) {
createStoreInstance();
}
const currentScope = pinia._scopes.get(id);
const trackedPaths = useRef(/* @__PURE__ */ new Set());
trackedPaths.current.clear();
const subscribe = useCallback(
(onStoreChange) => {
const listener = (_state, _prevState, patches) => {
let shouldUpdate = false;
for (const path of trackedPaths.current) {
const topKey = path.split(".")[0];
if (topKey in getters) {
if (!currentScope.getterCache.has(topKey)) {
shouldUpdate = true;
break;
}
} else {
if (patches.length > 0 && isAffected(patches, /* @__PURE__ */ new Set([path]))) {
shouldUpdate = true;
break;
}
}
}
if (shouldUpdate) {
onStoreChange();
}
};
currentScope.listeners.add(listener);
return () => currentScope.listeners.delete(listener);
},
[currentScope]
);
const getSnapshot = useCallback(() => currentScope.currentState, [currentScope]);
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const trackingProxy = currentScope.createStoreProxy((path) => {
trackedPaths.current.add(path.join("."));
});
return trackingProxy;
}
return { useStore, getStore };
}
export {
createPinia,
defineStore,
getActivePinia,
setActivePinia
};
//# sourceMappingURL=index.js.map