swr icon indicating copy to clipboard operation
swr copied to clipboard

Version 1 local storage provider does not work with version 2

Open Nosherwan opened this issue 1 year ago • 4 comments

Bug report

I am using the following provider that used to work with version 1.XX

interface Window {
	addEventListener: any;
}

declare const window: Window;

import { State } from 'swr';

export function localStorageProvider() {
	if (typeof window === 'undefined') {
		return new Map([]) as Map<string, State<any, any>>;
	}

	// When initializing, we restore the data from `localStorage` into a map.
	const map: Map<string, State<any, any>> = new Map(
		JSON.parse(localStorage.getItem('app-cache') || '[]')
	);

	// Before unloading the app, we write back all the data into `localStorage`.
	window.addEventListener('beforeunload', () => {
		const appCache = JSON.stringify(Array.from(map.entries()));
		localStorage.setItem('app-cache', appCache);
	});

	// We still use the map for write & read for performance.
	return map;
}

Description / Observed Behavior

Using the above localStorage provider with version 2.0.0.beta-6 it throws the following error:

Error: Hydration failed because the initial UI does not match what was rendered on the server.

What kind of issues did you encounter with SWR?

Expected Behavior

Error should not be thrown provider should be excepted.

CodeSandBox Link

https://codesandbox.io/s/friendly-brattain-kpbfus?file=/pages/index.js

Additional Context

If I comment out the provider assignment in SWRConfig at the top level of the app, then everything works fine. Seems like it is not accepting the custom local storage provider above.

SWR version. 2.0.0.beta-6 next 12.2.5 react 18.2.0

Nosherwan avatar Aug 17 '22 05:08 Nosherwan

Thanks for the suggestion @blackmann.

I tried creating a cache variable as you suggested, and tried 2 options:

1 - Moving it to the top of the main page function or 2 - Out of the function just after all the imports

In both scenarios the error still remains.

Nosherwan avatar Oct 16 '22 03:10 Nosherwan

Hi, I also met this error and cannot use localStorageProvider because of hydration error. @blackmann 's solution also didn't work. Could there be any update for it?

I used:

  • swr: 2.0.0-rc.0
  • next: 12.3.1
  • react: 18.2.0

jeina7 avatar Oct 20 '22 02:10 jeina7

The issue is still present on version 2.1.2, but I have found a workaround that can help.

To implement the solution, you need to define the localStorageProvider function as shown in the documentation and load it after mount. You don't need to check for window, as it will run on the client anyway. Here's the code:

function localStorageProvider() {
  const appCache = localStorage.getItem('app-cache') || '[]';
  const map = new Map(JSON.parse(appCache) as Iterable<[string, State<any, any>]>);
  window.addEventListener('beforeunload', () => {
    const appCache = JSON.stringify(Array.from(map.entries()));
    localStorage.setItem('app-cache', appCache);
  });
  return map;
}

// Load the localStorageProvider function after mount
const cacheProvider = useRef<typeof localStorageProvider>();

useEffect(() => {
  cacheProvider.current = localStorageProvider;
}, []);

<SWRConfig value={{ provider: cacheProvider.current }}>
  <App/>
</SWRConfig>

nekusu avatar Apr 11 '23 13:04 nekusu

I noticed an issue with the previous solution I provided. While it successfully implemented caching using local storage without hydration errors, it failed to load the cache on the first website load, which resulted in all SWR hooks without any data. To fix this, I suggest using a new useCacheProvider hook that initializes only the cache, rather than the entire function, after mount.

Here's the new code:

// useCacheProvider hook
function useCacheProvider() {
  const cache = useRef<Map<string, State<any, any>>>(new Map());

  useEffect(() => {
    const appCache = localStorage.getItem('app-cache');
    if (appCache) {
      const map = new Map(JSON.parse(appCache) as Iterable<[string, State<any, any>]>);
      map.forEach((value, key) => cache.current.set(key, value));
    }

    const saveCache = () => {
      const appCache = JSON.stringify(Array.from(cache.current.entries()));
      localStorage.setItem('app-cache', appCache);
    };

    window.addEventListener('beforeunload', saveCache);
    return () => window.removeEventListener('beforeunload', saveCache);
  }, []);

  return () => cache.current;
}

// use hook in SWRConfig
const provider = useCacheProvider();

<SWRConfig value={{ provider }}>
  <App/>
</SWRConfig>

nekusu avatar Apr 12 '23 07:04 nekusu