React/Preact hook: usePersistedStore

Persist any data in your application in localStorage between tabs and windows in real time via this great hook I wrote.

Implementation #

You can find the useEventListener hook used in this snippet here.

usePersistedStore.hook.ts

// React
import { useRef, useState, useEffect } from 'react';

// Preact
import { useRef, useState, useEffect } from 'preact/hooks';

import { useEventListener } from './useEventListener.hook';

import { dataPersist, PersistedCallback } from './dataPersist.helper';
import { isDifferent } from './isDifferent.helper';
import storage from './storage.helper';

export function usePersistedStore<T>(key: string, initialValue: T) {
const globalState = useRef<PersistedCallback | null>(null);

const [storeState, setStoreState] = useState(() =>
storage.get(key, initialValue)
);

// Automatically update store state on storage events
useEventListener('storage', (event: StorageEvent) => {
if (event.key === key && event.newValue) {
try {
const newStoreState = JSON.parse(event.newValue);

if (isDifferent(newStoreState, storeState)) {
setStoreState(newStoreState);
}
} catch (e) {
console.error(e);
}
}
});

useEffect(() => {
// Subscribe to global persistance changes on mount
globalState.current = dataPersist(key, setStoreState, initialValue);

return (): void => globalState.current?.unsubscribe();
}, []);

useEffect(() => {
storage.set(key, storeState);

// Tell everybody else this store state has changed
// (so they can update via their subscription)
globalState.current?.emit(storeState);
}, [storeState]);

return [storeState, setStoreState];
}

dataPersist.helper.ts

const persistance: {
[persistanceKey: string]: { callbacks: Function[]; value: any };
} = {};

export interface PersistedCallback {
unsubscribe: () => void;
emit: (value: any) => void;
}

export function dataPersist(
key: string,
callback: Function,
initialValue?: any
): PersistedCallback {
if (!persistance[key]) {
persistance[key] = { callbacks: [], value: initialValue };
}

persistance[key].callbacks.push(callback);

return {
unsubscribe(): void {
const callbacks = persistance[key].callbacks;
const index = callbacks.indexOf(callback);

if (index !== -1) {
callbacks.splice(index, 1);
}
},

emit(value: any): void {
if (isDifferent(value, persistance[key].value)) {
persistance[key].value = value;

let iterator = persistance[key].callbacks.length;

while (iterator--) {
const cb = persistance[key].callbacks[iterator];

if (cb !== callback) {
cb(value);
}
}
}
}
};
}

isDifferent.helper.ts

// Compares any two values of any kind and tells you if they're different
export function isDifferent(value: any, other: any): boolean {
// Values are of differing types
if (typeof value !== typeof other) {
return true;
}

// Values are not objects or arrays (primitives)
if (typeof value !== 'object') {
return value !== other;
}

// Values are both arrays
if (value instanceof Array && other instanceof Array) {
// Values are arrays of different length
if (value.length !== other.length) {
return true;
}

// Comparing array values
let iterator = value.length;

while (iterator--) {
if (isDifferent(value[iterator], other[iterator])) {
return true;
}
}
}
// One of the values is an array but the other isn't
else if (
(value instanceof Array && !(other instanceof Array)) ||
(other instanceof Array && !(value instanceof Array))
) {
return true;
}

// Both are objects
const keysValue = Object.keys(value);
const keysOther = Object.keys(other);

// They have a different amount of keys
if (keysValue.length !== keysOther.length) {
return true;
}

// Checking they have the same keys
let iterator = keysValue.length;

while (iterator--) {
const key = keysValue[iterator];

if (keysOther.includes(key)) {
return true;
}

// Comparing key values
if (isDifferent(value[key], other[key])) {
return true;
}
}

return false;
}

storage.helper.ts

/* eslint-disable no-empty */
export default {
get(key: string, initialValue?: any): any {
if (!('localStorage' in window)) {
return initialValue;
}

try {
const value = localStorage.getItem(key);

return value ? JSON.parse(value) : initialValue;
} catch (e) {
return initialValue;
}
},

set(key: string, value: any): void {
if (!('localStorage' in window)) {
return;
}

try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {}
}
};

Usage #

// React
import React, { FunctionComponent } from 'react';

// Preact
import { h, FunctionalComponent as FunctionComponent } from 'preact';

import { usePersistedStore } from './usePersistedStore.hook';

const Example: FunctionComponent = () => {
const [count, setCount] = usePersistedStore('count', 0);
const increment = (): void => setCount(count + 1);

return (
<div>
<button onClick={increment}>Click Me</button>
Clicked {count} times.
</div>
);
};

export default Example;

Published in Code


You might enjoy these as well

  1. Easy Typewriter effect/animation with Javascript
  2. React/Preact hook: useEventListener
  3. Using Styled components with Preact
  4. Load a specific image depending on light/dark mode
  5. Using Angular CLI with Laravel
  6. Deploy to Netlify via GitHub actions and "$ave Dat Money"
  7. Easily preserve any aspect ratio with pure CSS
  8. Easy Light and Dark mode switching with CSS