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;