Skip to content

Commit 52a71a5

Browse files
authored
Hydration fix (#18)
1 parent 3cb2880 commit 52a71a5

File tree

8 files changed

+438
-315
lines changed

8 files changed

+438
-315
lines changed

examples/nextjs/src/app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {Metadata} from 'next';
22
import {Geist, Geist_Mono} from 'next/font/google';
33
import {NavBar} from '@/components/nav/NavBar';
44
import {ThemeProvider} from '@/components/nav/ThemeProvider';
5+
import {StoreHydrator} from '@/components/shared/StoreHydrator';
56
import './globals.css';
67

78
const geistSans = Geist({
@@ -26,15 +27,17 @@ export default function RootLayout({
2627
children: React.ReactNode;
2728
}>) {
2829
return (
29-
<html lang="en" suppressHydrationWarning={true}>
30+
<html lang="en">
3031
<body
3132
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3233
>
33-
<ThemeProvider />
34-
<NavBar />
35-
<main className="mx-auto max-w-6xl px-4 sm:px-6 py-8 pb-16">
36-
{children}
37-
</main>
34+
<StoreHydrator>
35+
<ThemeProvider />
36+
<NavBar />
37+
<main className="mx-auto max-w-6xl px-4 sm:px-6 py-8 pb-16">
38+
{children}
39+
</main>
40+
</StoreHydrator>
3841
</body>
3942
</html>
4043
);

examples/nextjs/src/app/settings/page.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {PersistControls} from '@/components/settings/PersistControls';
22
import {SettingsForm} from '@/components/settings/SettingsForm';
3-
import {StorageDebugPanel} from '@/components/settings/StorageDebugPanel';
43

54
export default function SettingsPage() {
65
return (
@@ -23,7 +22,7 @@ export default function SettingsPage() {
2322
<SettingsForm />
2423
<div className="space-y-6">
2524
<PersistControls />
26-
<StorageDebugPanel />
25+
{/*<StorageDebugPanel />*/}
2726
</div>
2827
</div>
2928
</div>

examples/nextjs/src/components/settings/SettingsForm.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,15 @@ import {ApiInfo} from '@/components/shared/ApiInfo';
55
import {settingsStore} from '@/stores/settings-store';
66

77
export function SettingsForm() {
8-
const snap = useStore(settingsStore);
8+
const store = useStore(settingsStore);
99

1010
return (
1111
<div className="bg-card border border-border rounded-lg p-4 space-y-4">
1212
<h2 className="font-semibold">Preferences</h2>
1313
<ApiInfo
1414
apis={['createClassyStore (plain object)', 'useStore (auto-tracked)']}
1515
description="Plain object store (not a class). Direct property mutation — no setter methods needed."
16-
code={`const settingsStore = createClassyStore({
17-
theme: 'system',
18-
fontSize: 16,
19-
compactMode: false,
20-
});
16+
code={`const settingsStore = create);
2117
// mutation: settingsStore.theme = 'dark';`}
2218
/>
2319
<p className="text-xs text-muted">
@@ -28,7 +24,7 @@ export function SettingsForm() {
2824
<span className="text-muted text-xs">Theme</span>
2925
<select
3026
className="border border-border bg-background rounded px-3 py-1.5 text-sm"
31-
value={snap.theme}
27+
value={store.theme}
3228
onChange={(e) => {
3329
settingsStore.theme = e.target.value as 'light' | 'dark' | 'system';
3430
}}
@@ -40,12 +36,14 @@ export function SettingsForm() {
4036
</label>
4137

4238
<label className="flex flex-col gap-1 text-sm">
43-
<span className="text-muted text-xs">Font Size: {snap.fontSize}px</span>
39+
<span className="text-muted text-xs">
40+
Font Size: {store.fontSize}px
41+
</span>
4442
<input
4543
type="range"
4644
min={12}
4745
max={24}
48-
value={snap.fontSize}
46+
value={store.fontSize}
4947
onChange={(e) => {
5048
settingsStore.fontSize = Number(e.target.value);
5149
}}
@@ -56,7 +54,7 @@ export function SettingsForm() {
5654
<label className="flex items-center gap-2 text-sm">
5755
<input
5856
type="checkbox"
59-
checked={snap.compactMode}
57+
checked={store.compactMode}
6058
onChange={() => {
6159
settingsStore.compactMode = !settingsStore.compactMode;
6260
}}
@@ -68,7 +66,7 @@ export function SettingsForm() {
6866
<label className="flex items-center gap-2 text-sm">
6967
<input
7068
type="checkbox"
71-
checked={snap.notifications}
69+
checked={store.notifications}
7270
onChange={() => {
7371
settingsStore.notifications = !settingsStore.notifications;
7472
}}
@@ -82,7 +80,7 @@ export function SettingsForm() {
8280
<input
8381
type="number"
8482
className="border border-border bg-background rounded px-3 py-1.5 text-sm w-24"
85-
value={snap.defaultServings}
83+
value={store.defaultServings}
8684
onChange={(e) => {
8785
settingsStore.defaultServings = Number(e.target.value) || 1;
8886
}}
@@ -93,7 +91,7 @@ export function SettingsForm() {
9391
<span className="text-muted text-xs">Measurement Unit</span>
9492
<select
9593
className="border border-border bg-background rounded px-3 py-1.5 text-sm"
96-
value={snap.measurementUnit}
94+
value={store.measurementUnit}
9795
onChange={(e) => {
9896
settingsStore.measurementUnit = e.target.value as
9997
| 'metric'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import {type ReactNode, useEffect, useState} from 'react';
4+
import {settingsPersist} from '@/stores/settings-store';
5+
import {shoppingPersist} from '@/stores/shopping-store';
6+
7+
const persists = [settingsPersist, shoppingPersist];
8+
9+
export function StoreHydrator({children}: {children: ReactNode}) {
10+
const [hydrated, setHydrated] = useState(false);
11+
12+
useEffect(() => {
13+
Promise.all(persists.map((p) => p.rehydrate())).then(() =>
14+
setHydrated(true),
15+
);
16+
}, []);
17+
18+
if (!hydrated) {
19+
return null;
20+
}
21+
22+
return <>{children}</>;
23+
}

examples/nextjs/src/stores/planner-store.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ export const plannerPersist = persist(plannerStore, {
9292
return state;
9393
},
9494
merge: 'shallow',
95-
skipHydration: true,
9695
syncTabs: true,
9796
expireIn: 7 * 24 * 60 * 60 * 1000,
9897
clearOnExpire: true,

examples/nextjs/src/stores/settings-store.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import {createClassyStore} from '@codebelt/classy-store';
22
import {devtools, persist} from '@codebelt/classy-store/utils';
33

4-
export const settingsStore = createClassyStore({
5-
theme: 'system' as 'light' | 'dark' | 'system',
6-
fontSize: 16,
7-
compactMode: false,
8-
notifications: true,
9-
defaultServings: 4,
10-
measurementUnit: 'metric' as 'metric' | 'imperial',
11-
});
4+
class SettingsStore {
5+
theme = 'system' as 'light' | 'dark' | 'system';
6+
fontSize = 16;
7+
compactMode = false;
8+
notifications = true;
9+
defaultServings = 4;
10+
measurementUnit = 'metric' as 'metric' | 'imperial';
11+
}
12+
13+
export const settingsStore = createClassyStore(new SettingsStore());
1214

1315
export const settingsPersist = persist(settingsStore, {
1416
name: 'classy-kitchen-settings',

0 commit comments

Comments
 (0)