'set' on proxy: trap returned falsish for property 'userState:user'
Hey there,
So according to Nanostores docs, persistentMap / atom should be SSR compatible, but every. single. time. I use them, I get this error.
I am running Astro, with Vue, using SSR with Cloudflare, and if *any* component accidentally touches a nanostore's persistentAtom or persistentMap in SSR context, I get this error. It's super frustrating, almost impossible to fix easily, and inconsistent with what I would expect from the docs.
```ts
TypeError: 'set' on proxy: trap returned falsish for property 'userState:user'
at storeKey (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/@nanostores+persistent@0.10.2_nanostores@0.11.4/node_modules/@nanostores/persistent/index.js:106:35)
at store.setKey (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/@nanostores+persistent@0.10.2_nanostores@0.11.4/node_modules/@nanostores/persistent/index.js:111:5)
at restore (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/@nanostores+persistent@0.10.2_nanostores@0.11.4/node_modules/@nanostores/persistent/index.js:148:13)
at file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/@nanostores+persistent@0.10.2_nanostores@0.11.4/node_modules/@nanostores/persistent/index.js:153:5
at listener (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/nanostores@0.11.4/node_modules/nanostores/lifecycle/index.js:119:19)
at object.events.<computed>.reduceRight.shared (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/nanostores@0.11.4/node_modules/nanostores/lifecycle/index.js:16:58)
at Array.reduceRight (<anonymous>)
at file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/nanostores@0.11.4/node_modules/nanostores/lifecycle/index.js:16:31
at $store.listen (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/nanostores@0.11.4/node_modules/nanostores/lifecycle/index.js:127:9)
at Object.subscribe (file:///Users/zach/GitHub/socialaize/node_modules/.pnpm/nanostores@0.11.4/node_modules/nanostores/atom/index.js:71:26)
```
I fixed it previously by using a regular map in SSR contexts, but. I'm not doing anything crazy.
```ts
/**
* Persistent user state store using nanostores
* This is the single source of truth for all user-scoped data
* Uses localStorage for fast access (unencrypted)
*/
// Strictly use persistentMap; SSR is handled by @nanostores/persistent
const userStateStore = persistentMap<UserState>('userState:', DEFAULT_USER_STATE, {
encode: JSON.stringify,
decode: JSON.parse,
});
/**
* Global singleton composable for managing centralized user state
* Provides reactive access to user data with persistence via localStorage and IndexedDB
* IndexedDB stores encrypted data for security, localStorage for speed
*/
export const useUserState = createGlobalState(() => {
const state = useStore(userStateStore);
/**
* Save encrypted copy of user state to IndexedDB
* This provides a secure backup of sensitive user data
*/
async function syncToIndexedDB(state: UserState) {
if (typeof window === 'undefined') return;
try {
const encrypted = await encryptUserState(state);
await indexedDBCache.set('userProfiles', 'userState', encrypted);
} catch (error) {
console.warn('Failed to sync userState to IndexedDB:', error);
}
}
/**
* Load encrypted user state from IndexedDB
* Used as a fallback or initial load before API hydration
*/
async function loadFromIndexedDB(): Promise<UserState | null> {
if (typeof window === 'undefined') return null;
try {
const cached = await indexedDBCache.get<string>('userProfiles', 'userState');
if (cached?.data) {
return await decryptUserState(cached.data);
}
} catch (error) {
console.warn('Failed to load userState from IndexedDB:', error);
}
return null;
}
/**
* Hydrates user state from the backend authentication endpoint
* First attempts to load from IndexedDB cache, then fetches from API
*/
const hydrate = async () => {
try {
// Try loading from IndexedDB first
const cachedState = await loadFromIndexedDB();
if (cachedState) {
// Populate store with cached data
Object.entries(cachedState).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
userStateStore.setKey(key as keyof UserState, value);
}
});
}
// Then fetch fresh data from API
const res = await fetch('/api/auth/verify.json', { credentials: 'include' });
const data = await res.json();
if (data.success && data.data?.user) {
setUser(data.data.user);
if (data.data.session) setSession(data.data.session);
}
} catch (error) {
console.warn('Failed to hydrate user state:', error);
}
};
/**
* Sets the user in the state store
*/
const setUser = (user: Models.User<Models.Preferences>) => {
userStateStore.setKey('user', user);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the session in the state store
*/
const setSession = (session: Models.Session) => {
userStateStore.setKey('session', session);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the selected team ID in the state store
*/
const setSelectedTeam = (teamId: string) => {
userStateStore.setKey('selectedTeam', teamId);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the billing data in the state store
*/
const setBilling = (billing: Billing) => {
userStateStore.setKey('billing', billing);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the billing bonus data in the state store
*/
const setBillingBonus = (bonus: BillingBonus) => {
userStateStore.setKey('billingBonus', bonus);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the AI credits count in the state store
*/
const setAiCredits = (credits: number) => {
userStateStore.setKey('aiCredits', credits);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the OAuth accounts array in the state store
*/
const setOAuthAccounts = (accounts: OAuthAccount[]) => {
userStateStore.setKey('oauthAccounts', accounts);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the workflows array in the state store
*/
const setWorkflows = (workflows: Workflow[]) => {
userStateStore.setKey('workflows', workflows);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Sets the active workflow ID in the state store
*/
const setActiveWorkflow = (workflowId: string | null) => {
userStateStore.setKey('activeWorkflow', workflowId);
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
/**
* Clears all user state and resets to default values
*/
const clear = () => {
Object.entries(DEFAULT_USER_STATE).forEach(([key, value]) => {
userStateStore.setKey(key as keyof UserState, value);
});
syncToIndexedDB(userStateStore.get()).catch(console.warn);
};
return {
state,
hydrate,
setUser,
setSession,
setSelectedTeam,
setBilling,
setBillingBonus,
setAiCredits,
setOAuthAccounts,
setWorkflows,
setActiveWorkflow,
clear,
};
});
```
So, any advice or pointing in the right direction would be awesome
3 条评论