diff --git a/readme.md b/readme.md
index 3080265..0c53eaf 100644
--- a/readme.md
+++ b/readme.md
@@ -141,6 +141,44 @@ function useIsServerRender() {
+
+Preventing hydration errors when server default value should be different from client default value
+
+
+Using the `defaultValue` property to differ between server and client default values will cause hydration errors. To prevent them you can use the `defaultServerValue` property.
+
+```tsx
+// this will throw hydration error
+const [state, setState] = useLocalStorageState(
+ 'key',
+ {
+ defaultValue() {
+ if (isServer) {
+ return "server default value";
+ }
+ return "client default value";
+ },
+ },
+ );
+```
+
+`defaultServerValue` will overwrite `defaultValue` in the `useSyncExternalStore()` hook's `getSnapshot()` function.
+Difference is server's `getSnapshot()` will run on server and client's hydration, then `defaultValue` or local storage value will be used after that.
+
+```tsx
+const [state, setState] = useLocalStorageState(
+ 'key',
+ {
+ defaultValue: "client default value",
+ defaultServerValue: "server default value",
+ },
+ );
+```
+
+> Example use cases: You want to show a modal when the user's first enter and you are persistently storing that data on `localStorage`. Using `defaultServerValue` will allow you to show the modal only on the server.
+
+
+
## API
#### `useLocalStorageState(key: string, options?: LocalStorageOptions)`
@@ -165,6 +203,14 @@ Default: `undefined`
The default value. You can think of it as the same as `useState(defaultValue)`.
+#### `options.defaultServerValue`
+
+Type: `any`
+
+Default: `undefined`
+
+The default value for the server if it should be different from the client's default value. Server will use `defaultValue` if `defaultServerValue` is not provided.
+
#### `options.storageSync`
Type: `boolean`
diff --git a/src/useLocalStorageState.ts b/src/useLocalStorageState.ts
index 47e884b..96723b9 100644
--- a/src/useLocalStorageState.ts
+++ b/src/useLocalStorageState.ts
@@ -6,6 +6,7 @@ export const inMemoryData = new Map()
export type LocalStorageOptions = {
defaultValue?: T | (() => T)
+ defaultServerValue?: T | (() => T)
storageSync?: boolean
serializer?: {
stringify: (value: unknown) => string
@@ -42,9 +43,11 @@ export default function useLocalStorageState(
): LocalStorageState {
const serializer = options?.serializer
const [defaultValue] = useState(options?.defaultValue)
+ const [defaultServerValue] = useState(options?.defaultServerValue)
return useLocalStorage(
key,
defaultValue,
+ defaultServerValue,
options?.storageSync,
serializer?.parse,
serializer?.stringify,
@@ -54,6 +57,7 @@ export default function useLocalStorageState(
function useLocalStorage(
key: string,
defaultValue: T | undefined,
+ defaultServerValue: T | undefined,
storageSync: boolean = true,
parse: (value: string) => unknown = parseJSON,
stringify: (value: unknown) => string = JSON.stringify,
@@ -125,7 +129,7 @@ function useLocalStorage(
},
// useSyncExternalStore.getServerSnapshot
- () => defaultValue,
+ () => defaultServerValue ?? defaultValue,
)
const setState = useCallback(
(newValue: SetStateAction): void => {
diff --git a/test/browser.test.tsx b/test/browser.test.tsx
index cdd4d8c..07503e2 100644
--- a/test/browser.test.tsx
+++ b/test/browser.test.tsx
@@ -58,12 +58,34 @@ describe('useLocalStorageState()', () => {
expect(todos).toStrictEqual(['first', 'second'])
})
- test(`initial state is written to localStorage`, () => {
+ test('initial state is written to localStorage', () => {
renderHook(() => useLocalStorageState('todos', { defaultValue: ['first', 'second'] }))
expect(localStorage.getItem('todos')).toStrictEqual(JSON.stringify(['first', 'second']))
})
+ test('should return defaultValue instead of defaultServerValue on the browser', () => {
+ const { result } = renderHook(() =>
+ useLocalStorageState('todos', {
+ defaultValue: ['first', 'second'],
+ defaultServerValue: ['third', 'forth'],
+ }),
+ )
+
+ const [todos] = result.current
+ expect(todos).toStrictEqual(['first', 'second'])
+ })
+
+ test('defaultServerValue should not written to localStorage', () => {
+ renderHook(() =>
+ useLocalStorageState('todos', {
+ defaultServerValue: ['third', 'forth'],
+ }),
+ )
+
+ expect(localStorage.getItem('todos')).toStrictEqual(null)
+ })
+
test('updates state', () => {
const { result } = renderHook(() =>
useLocalStorageState('todos', { defaultValue: ['first', 'second'] }),
diff --git a/test/server.test.tsx b/test/server.test.tsx
index 1b64110..ed5c7c5 100644
--- a/test/server.test.tsx
+++ b/test/server.test.tsx
@@ -59,13 +59,38 @@ describe('useLocalStorageState()', () => {
}),
)
- expect(result.current[0]).toEqual(['first', 'second'])
+ const [todos] = result.current
+ expect(todos).toStrictEqual(['first', 'second'])
})
test('returns default value on the server', () => {
const { result } = renderHookOnServer(() => useLocalStorageState('todos'))
- expect(result.current[0]).toEqual(undefined)
+ const [todos] = result.current
+ expect(todos).toBe(undefined)
+ })
+
+ test('returns defaultServerValue on the server', () => {
+ const { result } = renderHookOnServer(() =>
+ useLocalStorageState('todos', {
+ defaultServerValue: ['third', 'forth'],
+ }),
+ )
+
+ const [todos] = result.current
+ expect(todos).toStrictEqual(['third', 'forth'])
+ })
+
+ test('defaultServerValue should overwrite defaultValue on the server', () => {
+ const { result } = renderHookOnServer(() =>
+ useLocalStorageState('todos', {
+ defaultValue: ['first', 'second'],
+ defaultServerValue: ['third', 'forth'],
+ }),
+ )
+
+ const [todos] = result.current
+ expect(todos).toStrictEqual(['third', 'forth'])
})
test(`setValue() on server doesn't throw`, () => {