Closed Suraj-Tiwari closed 2 years ago
@ai can you please have a look?
Show your code, where do you use nanostores
Vue
<template>
<div>Vue Count: {{ $counter }}</div>
<button @click="counter.set($counter - 1)">-</button>
<button @click="counter.set($counter + 1)">+</button>
</template>
<script setup>
import {counter} from "../store";
import {useStore} from "@nanostores/vue";
const $counter = useStore(counter);
</script>
<style scoped></style>
React
import React from "react";
import {counter} from "../store";
import {useStore} from "@nanostores/react";
export default function CounterReact({demo}) {
const $counter = useStore(counter);
let add = () => {
counter.set($counter + 1)
};
let subtract = () => {
counter.set($counter - 1)
};
return (
<div>
React Count: {$counter} {demo}
<br/>
<button onClick={subtract}>-</button>
<button onClick={add}>+</button>
</div>
);
}
Store File
import {persistentAtom} from '@nanostores/persistent'
export const counter = persistentAtom<number>('counter', 0, {
encode: JSON.stringify,
decode(value) {
try {
return JSON.parse(value)
} catch (e) {
return value
}
},
})
and this is how i'm using it in astro file
<ReactCounter client:load />
<VueCounter client:load />
This error happens because user’s localStorage
will have unique data not equal to the server data.
It is not related to Nano Stores Persistent but in a way how rehydration works with localStorage
in general.
@ai I would humbly request to re-open this issue as I am struggling with this issue too.
You are correct that this is an issue inherent with how hydration + localStorage
works. This post gives a good explanation on how to address it: https://www.benmvp.com/blog/handling-react-server-mismatch-error/
However, when I try to use that solution with the useStore
hooks in useEffect
we get an invalid hooks call warning.
It would be helpful for me and the original poster (and others I'm sure) if we could post a solution on how to use persistent
with Astro/SSR + React or Vue to avoid getting the mismatch hydration error.
Here's how I solved the mismatch error (super simplified example). This is React so will need to be adjusted for Vue.
We need to wait until the window
object is present to get data from localStorage
so we grab the persistent
data in useEffect()
. Since we can't use the useStore()
hook in useEffect()
we need to use the nanostores
get()
method.
useEffect(() => {
const storeData = myStore.get() // can't use useStore hook here
if (storeData) {
setData(storeData)
}
}, [myStore.get()]) // <-- using function here; this was the trick to get state to update on changes without using useStore hook
Not sure if this is following best practices but no hydration mismatch errors and state is updated when the data changes.
Why do you this way instead of storeDate = useStore(myStore)
?
let data = useStore(myStore)
useEffect(() => {
if (data) {
setData(data)
}
}, [data])
@ai so simple...so effective. That totally works. Thank you.
I know this is an older ticket but I ended up here looking for an answer as well and this may help someone else down the line.
I'm guessing that the example from @ai also has a const [,setData] = useState()
for the setData()
call in the file? So that's data
from the useStore()
and setData
from the useState()
?
I made a hook that passes along the existing value once the React component is rendered:
// useLazyStore.ts
import { useEffect, useState } from 'react';
import { useStore } from '@nanostores/react';
import type { ReadableAtom, WritableAtom } from 'nanostores';
function useLazyStore<T>($atom: ReadableAtom<T> | WritableAtom<T>, initial: T): [T, boolean] {
const atomValue = useStore($atom);
const [hasIgnition, setHasIgnition] = useState(false);
useEffect(() => {
setHasIgnition(true);
}, []);
return [hasIgnition ? atomValue : initial, hasIgnition];
}
export default useLazyStore;
I like this method better as it's almost the same API as the original useStore()
but you will need to pass in the initial
value so that the rendered output matches before hydration.
// SomeComponent.tsx
const [showFilters] = useLazyStore($showFilters, showFiltersInitialValue);
Or if you want to know if the value is ready:
// SomeComponent.tsx
const [showFilters, isAvailable] = useLazyStore($showFilters, showFiltersInitialValue);
I export the initial value from my store to make this watertight:
// showFiltersStore.ts
import { persistentAtom } from '@nanostores/persistent';
export type ShowFiltersStateValue = boolean;
export const showFiltersInitialValue = true;
export const $showFilters = persistentAtom<ShowFiltersStateValue>('showFilters', showFiltersInitialValue, {
encode: (value: boolean) => {
return value === true ? 'true' : 'false';
},
decode: (value: string) => {
return value === 'true' ? true : false;
},
});
What do you think? Any changes necessary?
I would like to have the useLazyStore()
optionally accept undefined
for the initial
value. But I want the typescript to only allow undefined
for the exported atomValue
if the initial
value is undefined
. Otherwise I want typescript to state the atomValue
is based on the generic T
.
If initial
is undefined
then atomValue
is T | undefined
.
If initial
is provided then atomValue
is T
.
This works if you pass in undefined
but I would like to make the initial
argument optional and achieve the above typings.
Getting these errors + warnings (React & Vue) when using persistent