nanostores / persistent

Smart store for Nano Stores state manager to keep data in localStorage
MIT License
278 stars 18 forks source link

Hydration issue when using persistent #26

Closed Suraj-Tiwari closed 2 years ago

Suraj-Tiwari commented 2 years ago

Getting these errors + warnings (React & Vue) when using persistent

Screenshot 2022-09-02 at 12 11 38 PM
Suraj-Tiwari commented 2 years ago

@ai can you please have a look?

ai commented 2 years ago

Show your code, where do you use nanostores

Suraj-Tiwari commented 2 years ago

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 />
ai commented 2 years ago

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.

joshuaiz commented 2 years ago

@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.

joshuaiz commented 2 years ago

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.

ai commented 2 years ago

Why do you this way instead of storeDate = useStore(myStore)?

let data = useStore(myStore)
useEffect(() => {
    if (data) {
        setData(data)
    }
}, [data])
joshuaiz commented 2 years ago

@ai so simple...so effective. That totally works. Thank you.

tylers commented 1 year ago

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.