Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(injectLocalStorage): initial implementation of injectLocalStorage #295

Merged
merged 7 commits into from
Apr 23, 2024

Conversation

palexcast
Copy link
Contributor

@palexcast palexcast commented Mar 1, 2024

Solves #245

Adds a utility that synchronizes localStorage changes across tabs, and has an optional validate method to verify the validity of the object.

I can create a PR for a similar/same thing for sessionStorage if this seems like a good idea/implementation.
Would love some feedback before I do that though :)

Usage:

type Counter = {counter: number};

export class AppComponent {
	private counterStorage = injectLocalStorage<Counter>('SOME_KEY');
	
	onClick(): void {
	    const value = this.counterStorage() ?? { counter: 0 };
	    this.counterStorage.set({ counter: value.counter + 1 });
	}
}

// AppComponent.html

<span>{{ counter().counter }}</span>
@if(counter.error(); as error) {
	<span>An error occurred while parsing localStorage: {{ error }}</span>
}
<button (click)="onClick()">increment</button>

Copy link

nx-cloud bot commented Mar 1, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 511db52. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 4 targets

Sent with 💌 from NxCloud.

@ilirbeqirii
Copy link
Contributor

Should this utility function also, follow the "inject***" naming pattern like other functions? @eneajaho

@palexcast
Copy link
Contributor Author

palexcast commented Mar 10, 2024

Should this utility function also, follow the "inject***" naming pattern like other functions? @eneajaho

I've got no strong opinion on the naming of this, I can fix it if you want it to change :)

I also have some stuff I want to clean up, like passing the validate func as a part of an optional "options" param, as there might be other stuff to add, and it fits the "signal" way of passing optional options.

@eneajaho
Copy link
Collaborator

Hello @palexcast
Thanks for the PR.

Wanted to link this other implementation here https://twitter.com/_wallstreetdev/status/1767693809503084873

Would like to start a conversation about these implementations and find the best way to go forward.

@wall-street-dev
Copy link

Hello WSD here :)
Disclaimer: I just liked a lot Mike Hartington's idea but my code is also amateurish. I'm just starting with signals :)
In terms of the implementation it really depends on the direction you guys want to take. The approach I've shared is heavily inspired by React use-local-storage-state and is 100% based on Signals. It also lacks of In-Memory-Data support and proper error handling. This is all I have so far:

import { effect, signal, WritableSignal } from '@angular/core';

export interface LocalStorageOptions<T> {
  defaultValue?: T | (() => T);
  storageSync?: boolean;
  serializer?: {
    stringify: (value: unknown) => string;
    parse: (value: string) => unknown;
  };
}

function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
  return typeof value === 'function';
}

function goodTry<T>(tryFn: () => T): T | undefined {
  try {
    return tryFn();
  } catch {
    return undefined;
  }
}

function parseJSON(value: string): unknown {
  return value === 'undefined' ? undefined : JSON.parse(value);
}

export function localStorageSignal<T>(key: string, options?: LocalStorageOptions<T>): WritableSignal<T | undefined> {
  const defaultValue = isFunction(options?.defaultValue) ? options.defaultValue() : options?.defaultValue;
  const serializer = options?.serializer || {
    stringify: JSON.stringify,
    parse: parseJSON
  };
  const storageSync = options?.storageSync ?? true;
  const initialStoredValue = goodTry(() => localStorage.getItem(key));
  const initialValue = initialStoredValue ? goodTry(() => serializer.parse(initialStoredValue) as T) : defaultValue;
  const internalSignal = signal<T | undefined>(initialValue);

  effect(() => {
    const value = internalSignal();
    if (value === undefined) {
      goodTry(() => localStorage.removeItem(key));
    } else {
      goodTry(() => localStorage.setItem(key, serializer.stringify(value)));
    }
  });

  if (storageSync) {
    const onStorage = (event: StorageEvent) => {
      if (event.storageArea === localStorage && event.key === key) {
        const newValue = event.newValue !== null ? (serializer.parse(event.newValue) as T) : undefined;
        internalSignal.set(newValue);
      }
    };
    window.addEventListener('storage', onStorage);
    effect(onCleanup => {
      onCleanup(() => window.removeEventListener('storage', onStorage));
    });
  }

  return internalSignal;
}

@palexcast
Copy link
Contributor Author

I'll take a look at both those implementations and see if I can adjust some of my implementation with some input from those.

If there is any more input/wishes regarding the direction of the implementation that would be greatly appreciated :)

@palexcast
Copy link
Contributor Author

@guzmanoj So I finally had some time, and the more I look at your implementation I really like it. It's clean and functional.
I really don't feel comfortable just copy-pasting it and taking credit for it though, the credit should be yours ;)

For now I copy-pasted your solution, and renamed it to injectLocalStorage like @ilirbeqirii mentioned, as it does adhere more to the convention of the lib.

In regards to error handling, I'm not sure about what the "signal" convention is regarding this.
Should goodTry throw an error, or just return undefined as the current implementation does.
Maybe @eneajaho has some thoughts?

My current thought regarding this, is that the parse function can be used to handle validation/errors.
One of the tests demonstrate this.

I also unwrapped the serializer so you don't need to pass both parse and stringify. Adding docs instead should be enough to explain that they act as serializers. (just my opinion).

I've also adjusted the tests, but want to add some more, would be nice to get some feedback before moving on though.

@eneajaho
Copy link
Collaborator

Can you also update the PR description, so it is the same as the updated code we have?

@palexcast
Copy link
Contributor Author

@eneajaho Updated the PR in regards to your comments :D

@eneajaho eneajaho force-pushed the feat/use-local-storage branch from 98190fc to 9657ec1 Compare April 23, 2024 15:14
@eneajaho eneajaho changed the title feat(useLocalStorage): Initial implementation of useLocalStorage feat(injectLocalStorage): initial implementation of injectLocalStorage Apr 23, 2024
@nartc nartc merged commit e995dcb into ngxtension:main Apr 23, 2024
2 checks passed
@eneajaho
Copy link
Collaborator

Thanks for this @palexcast 🙌

@palexcast palexcast deleted the feat/use-local-storage branch April 24, 2024 06:18
@palexcast
Copy link
Contributor Author

Thanks for the reviews and help getting this merged @eneajaho :D
First 'real' contribution to OS, so greatly appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants