Skip to content

Commit ee17336

Browse files
authored
Merge pull request #209 from favour-GL/feat/Zustand-v5-incompatibility-in-auth-store.js-and-possibly-others
feat:Zustand v5 incompatibility in auth-store.js and possibly others
2 parents 75fc199 + f768a02 commit ee17336

3 files changed

Lines changed: 63 additions & 20 deletions

File tree

frontend/store/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Zustand Store Conventions
2+
3+
## Persist keys must be unique
4+
5+
Every store that uses Zustand's `persist` middleware writes to `localStorage`
6+
(or another storage backend) under the key passed as `name` in the persist
7+
config. **Two stores must never share a `name`.** If they do, whichever store
8+
hydrates second on mount silently overwrites the other's persisted data —
9+
this happened once already (see issue #053) and cost players their progress.
10+
11+
### Naming convention
12+
13+
Use `<store-name>:v<version>`, where `<store-name>` matches the file/hook
14+
name and `<version>` matches the persist `version` field in that store's
15+
config:
16+
17+
| Store file | Hook | Persist `name` |
18+
| -------------------------------------- | ---------------------- | ------------------------ |
19+
| `useGameStore.js` | `useGameStore` | `game-store:v1` |
20+
| `game-progress/game-progress-store.js` | `useGameProgressStore` | `game-progress-store:v1` |
21+
22+
When you bump a store's `version` (e.g. because you changed the shape of
23+
persisted state and need a migration), bump the `:vN` suffix in `name` too,
24+
so old and new shapes never collide under the same key.
25+
26+
### Adding a new persisted store
27+
28+
1. Pick a `name` following the table format above.
29+
2. Grep the repo for that exact string before committing, to confirm no
30+
other `persist({ name: ... })` call already uses it:
31+
32+
```bash
33+
grep -rn "name: ['\"]your-new-key" frontend/store
34+
```
35+
36+
3. Add a row to the table in this README.
37+
38+
### Note on consolidation
39+
40+
Issue #061 proposes merging `useGameStore` and `useGameProgressStore` into a
41+
single store. If/when that lands, this file should be updated to reflect the
42+
single resulting store and its persist key.

frontend/store/game-progress/game-progress-store.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import create from 'zustand';
2-
import { persist } from 'zustand/middleware';
1+
import create from "zustand";
2+
import { persist } from "zustand/middleware";
33

4-
const useGameStore = create(
4+
const useGameProgressStore = create(
55
persist(
66
(set) => ({
77
completedPuzzles: [],
@@ -27,10 +27,10 @@ const useGameStore = create(
2727
}),
2828
}),
2929
{
30-
name: 'game-storage', // unique name for storage
30+
name: "game-progress-store:v1", // unique name for storage — do not reuse 'game-storage'
3131
getStorage: () => localStorage, // (optional) by default, 'localStorage' is used
32-
}
33-
)
32+
},
33+
),
3434
);
3535

36-
export default useGameStore;
36+
export default useGameProgressStore;

frontend/store/useGameStore.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import axios from "axios";
1818
* not move the factory invocation inside `setItem` or the debounce
1919
* will be defeated by per-call instance re-creation.
2020
*/
21+
2122
const createThrottledStorage = (storage, delayMs = 150) => {
2223
let timer = null;
2324
let pendingValue = null;
@@ -90,7 +91,7 @@ const useGameStore = create(
9091
const response = await axios.post(
9192
"http://localhost:4001/auth/register",
9293
{ username, password },
93-
{ withCredentials: true }
94+
{ withCredentials: true },
9495
);
9596
set({ user: response.data });
9697
} catch (error) {
@@ -104,7 +105,7 @@ const useGameStore = create(
104105
const response = await axios.post(
105106
"http://localhost:4001/auth/login",
106107
{ username, password },
107-
{ withCredentials: true }
108+
{ withCredentials: true },
108109
);
109110
set({ user: response.data });
110111
} catch (error) {
@@ -118,7 +119,7 @@ const useGameStore = create(
118119
await axios.post(
119120
"http://localhost:4001/auth/logout",
120121
{},
121-
{ withCredentials: true }
122+
{ withCredentials: true },
122123
);
123124
set({
124125
user: null,
@@ -148,7 +149,7 @@ const useGameStore = create(
148149

149150
const newCompletedPuzzles = [...completedPuzzles, puzzleId];
150151
const currentDifficultyPuzzles = newCompletedPuzzles.filter((id) =>
151-
id.startsWith(currentDifficulty)
152+
id.startsWith(currentDifficulty),
152153
);
153154

154155
const isLevelCompleted = currentDifficultyPuzzles.length === 5;
@@ -182,7 +183,7 @@ const useGameStore = create(
182183
currentPuzzleIndex: nextPuzzleIndex,
183184
score: newScore,
184185
},
185-
{ withCredentials: true }
186+
{ withCredentials: true },
186187
);
187188

188189
set({
@@ -208,7 +209,7 @@ const useGameStore = create(
208209
userId: user.id,
209210
nft,
210211
},
211-
{ withCredentials: true }
212+
{ withCredentials: true },
212213
);
213214

214215
set({ nfts: [...nfts, nft] });
@@ -230,7 +231,7 @@ const useGameStore = create(
230231
{
231232
params: { page, limit },
232233
withCredentials: true,
233-
}
234+
},
234235
);
235236

236237
const data = response.data || {};
@@ -244,7 +245,7 @@ const useGameStore = create(
244245
const existing = get().nfts || [];
245246
const seen = new Set(existing.map((n) => n.id));
246247
const merged = existing.concat(
247-
items.filter((n) => n && !seen.has(n.id))
248+
items.filter((n) => n && !seen.has(n.id)),
248249
);
249250
set({ nfts: merged });
250251
}
@@ -264,7 +265,7 @@ const useGameStore = create(
264265
try {
265266
const response = await axios.get(
266267
`http://localhost:4001/user/${user.id}`,
267-
{ withCredentials: true }
268+
{ withCredentials: true },
268269
);
269270
set(response.data);
270271
} catch (error) {
@@ -281,7 +282,7 @@ const useGameStore = create(
281282
await axios.post(
282283
`http://localhost:4001/game/reset`,
283284
{ userId: user.id },
284-
{ withCredentials: true }
285+
{ withCredentials: true },
285286
);
286287
set({
287288
currentDifficulty: "easy",
@@ -301,7 +302,7 @@ const useGameStore = create(
301302
// Throttle writes so the localStorage payload is only re-serialised
302303
// and written once per coalescing window (see `createThrottledStorage`).
303304
storage: createJSONStorage(() =>
304-
createThrottledStorage(safeLocalStorage())
305+
createThrottledStorage(safeLocalStorage()),
305306
),
306307
// Only durable progress fields are persisted. Transient state (none
307308
// currently, but a narrow allow-list keeps the storage size small and
@@ -316,8 +317,8 @@ const useGameStore = create(
316317
nfts: state.nfts,
317318
}),
318319
version: 1,
319-
}
320-
)
320+
},
321+
),
321322
);
322323

323324
export default useGameStore;

0 commit comments

Comments
 (0)