Skip to content

Instantly share code, notes, and snippets.

@erdesigns-eu
Created July 1, 2024 07:46
Show Gist options
  • Save erdesigns-eu/88700bf3e6572f838f211ecfadb12f23 to your computer and use it in GitHub Desktop.
Save erdesigns-eu/88700bf3e6572f838f211ecfadb12f23 to your computer and use it in GitHub Desktop.
TypeScript ClientStorage Class with LocalStorage and React Integration
/**
* A callback function that is called when a value changes.
* @callback SubscriptionCallback
* @param {any} value The new value.
* @returns {void}
*/
type SubscriptionCallback<T = any> = (value: T) => void;
/**
* A simple client-side storage class that uses localStorage to store values.
* @class ClientStorage
*/
class ClientStorage {
// A map of key to a set of subscription callbacks.
private subscriptions: Record<string, Set<SubscriptionCallback<any>>> = {};
/**
* Set a value in localStorage.
* @param {string} key The key to store the value under.
* @param {any} value The value to store.
*/
set<T>(key: string, value: T): void {
if (typeof localStorage !== 'undefined') {
// Get the type of the value.
let type: string = typeof value;
// Create a variable to store the value.
let storedValue: string | T = value;
// Convert the value to a string if it is not a string, boolean, or number.
if (value instanceof Date) {
type = 'date';
storedValue = value.toISOString();
} else if (typeof value === 'bigint') {
type = 'bigint';
storedValue = value.toString();
} else if (typeof value === 'object') {
storedValue = JSON.stringify(value);
}
// Store the value in localStorage.
localStorage.setItem(key, storedValue as string);
// Store the type of the value in localStorage.
localStorage.setItem(`${key}:type`, type);
// Call the subscription callbacks for the key if they exist.
if (this.subscriptions[key]) {
for (const callback of this.subscriptions[key]) {
callback(value);
}
}
}
}
/**
* Get a value from localStorage.
* @param {string} key The key to get the value for.
* @param {T} defaultValue The default value to return if the key does not exist.
* @returns {T} The value stored under the key, or the default value if the key does not exist.
*/
get<T>(key: string, defaultValue: T): T {
if (typeof localStorage !== 'undefined') {
// Get the value from localStorage.
const value = localStorage.getItem(key);
// If the value is null (i.e. the key does not exist), return the default value.
if (value === null) {
return defaultValue;
}
// Get the type of the value from localStorage.
const type = localStorage.getItem(`${key}:type`);
// If the type is null (i.e. the key does not exist), return the value as is (as a string).
if (!type) {
return value as unknown as T;
}
// Convert the value to the correct type.
switch (type) {
case 'boolean':
return (value === 'true') as unknown as T;
case 'number':
return parseFloat(value) as unknown as T;
case 'bigint':
return BigInt(value) as unknown as T;
case 'date':
return new Date(value) as unknown as T;
case 'object':
return JSON.parse(value) as T;
default:
return value as unknown as T;
}
}
// If localStorage is not available, return the default value.
return defaultValue;
}
/**
* Subscribe to changes to a value in localStorage.
* @param key The key to subscribe to.
* @param callback The callback to call when the value changes.
* @returns An object with a stop method that can be called to stop the subscription.
*/
subscribe<T>(key: string, callback: SubscriptionCallback<T>): { stop: () => void } {
// Create a new set of subscriptions if one does not exist for the key.
if (!this.subscriptions[key]) {
this.subscriptions[key] = new Set<SubscriptionCallback<any>>();
}
// Add the callback to the set of subscriptions.
this.subscriptions[key].add(callback);
// Return an object with a stop method that can be called to stop the subscription.
return {
stop: () => {
this.subscriptions[key].delete(callback);
},
};
}
}
export default ClientStorage;
@erdesigns-eu
Copy link
Author

erdesigns-eu commented Jul 1, 2024

Example Usage of ClientStorage

import ClientStorage from './ClientStorage'; // Adjust the import according to your file structure

// Create an instance of ClientStorage
const clientStorage = new ClientStorage();

// Set values
clientStorage.set<string>('username', 'john_doe');
clientStorage.set<boolean>('isLoggedIn', true);
clientStorage.set<number>('loginCount', 42);
clientStorage.set<Date>('lastLogin', new Date());
clientStorage.set<bigint>('bigNumber', BigInt(1234567890123456789));

// Get values with correct types
const username: string = clientStorage.get<string>('username', '');
const isLoggedIn: boolean = clientStorage.get<boolean>('isLoggedIn', false);
const loginCount: number = clientStorage.get<number>('loginCount', 0);
const lastLogin: Date = clientStorage.get<Date>('lastLogin', new Date());
const bigNumber: bigint = clientStorage.get<bigint>('bigNumber', BigInt(0));

console.log(username, isLoggedIn, loginCount, lastLogin, bigNumber);

@erdesigns-eu
Copy link
Author

Example Using ClientStorage in a React Component with Generics

import React, { useEffect, useState } from 'react';
import ClientStorage from './ClientStorage'; // Adjust the import according to your file structure

// Create an instance of ClientStorage
const clientStorage = new ClientStorage();

const App: React.FC = () => {
    const [username, setUsername] = useState<string>('');
    const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);

    useEffect(() => {
        // Retrieve persisted state with generics
        const storedUsername = clientStorage.get<string>('username', '');
        const storedIsLoggedIn = clientStorage.get<boolean>('isLoggedIn', false);

        setUsername(storedUsername);
        setIsLoggedIn(storedIsLoggedIn);

        // Subscribe to changes in localStorage with generics
        const usernameSubscription = clientStorage.subscribe<string>('username', setUsername);
        const isLoggedInSubscription = clientStorage.subscribe<boolean>('isLoggedIn', setIsLoggedIn);

        // Cleanup subscriptions on unmount
        return () => {
            usernameSubscription.stop();
            isLoggedInSubscription.stop();
        };
    }, []);

    const handleLogin = () => {
        const newUsername = 'john_doe';
        const loginStatus = true;
        setUsername(newUsername);
        setIsLoggedIn(loginStatus);
        clientStorage.set<string>('username', newUsername);
        clientStorage.set<boolean>('isLoggedIn', loginStatus);
    };

    const handleLogout = () => {
        const newUsername = '';
        const loginStatus = false;
        setUsername(newUsername);
        setIsLoggedIn(loginStatus);
        clientStorage.set<string>('username', newUsername);
        clientStorage.set<boolean>('isLoggedIn', loginStatus);
    };

    return (
        <div>
            {isLoggedIn ? (
                <div>
                    <p>Welcome, {username}!</p>
                    <button onClick={handleLogout}>Logout</button>
                </div>
            ) : (
                <div>
                    <p>Please log in.</p>
                    <button onClick={handleLogin}>Login</button>
                </div>
            )}
        </div>
    );
};

export default App;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment