Last active
March 28, 2026 05:07
-
-
Save fsubal/4836b25dd089183e672d38cca4cedf3b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| interface Action<T = unknown> { | |
| type: string; | |
| payload: T; | |
| } | |
| interface StateChangeDetail<A extends Action<unknown>, T> { | |
| action: A; | |
| previousState: T; | |
| nextState: T; | |
| } | |
| type StateChangeCallback<A extends Action<unknown>, T> = (event: CustomEvent<StateChangeDetail<A, T>>) => void; | |
| type StateEvent<T extends State<Action<unknown>, unknown>> = T extends State<infer A, infer S> ? CustomEvent<StateChangeDetail<A, S>> : never; | |
| abstract class State<A extends Action<unknown>, T> { | |
| #currentState: T; | |
| #events = new EventTarget() | |
| #onChangeCallbacks: WeakSet<StateChangeCallback<A, T>> = new WeakSet() | |
| constructor(initialState: T) { | |
| this.#currentState = initialState; | |
| } | |
| subscribe(callback: StateChangeCallback<A, T>) { | |
| this.#onChangeCallbacks.add(callback); | |
| this.#events.addEventListener('change', callback as EventListenerOrEventListenerObject); | |
| } | |
| unsubscribe(callback: StateChangeCallback<A, T>) { | |
| this.#events.removeEventListener('change', callback as EventListenerOrEventListenerObject); | |
| this.#onChangeCallbacks.delete(callback); | |
| } | |
| trigger<K extends A['type']>(type: K, payload: (A & { type: K })['payload']) { | |
| const action = { type, payload } as A & { type: K }; | |
| const previousState = this.#currentState; | |
| const nextState = this.handleTrigger(action, this.#currentState); | |
| this.#currentState = nextState; | |
| const event = new CustomEvent<StateChangeDetail<A, T>>('change', { detail: { action, previousState, nextState } }); | |
| this.#events.dispatchEvent(event); | |
| } | |
| abstract handleTrigger(action: A, currentState: T): T | |
| } | |
| type UserAction = | |
| | { type: 'USER_LOADED', payload: { id: number } } | |
| | { type: 'USER_SAVED', payload: { name: string } } | |
| type UserState = { id: number | null, name: string | null } | |
| class User extends State<UserAction, UserState> { | |
| handleTrigger(action: UserAction, currentState: UserState): UserState { | |
| switch (action.type) { | |
| case 'USER_LOADED': | |
| return { ...currentState, id: action.payload.id }; | |
| case 'USER_SAVED': | |
| return { ...currentState, name: action.payload.name }; | |
| default: | |
| return currentState; | |
| } | |
| } | |
| set name(name: string) { | |
| this.trigger('USER_SAVED', { name }); | |
| } | |
| async load() { | |
| const response = await fetch('https://api.example.com/users'); | |
| const data = await response.json(); | |
| this.trigger('USER_LOADED', { id: data.id }); | |
| } | |
| } | |
| const userState = new User({ id: null, name: null }); | |
| function handleStateChange(event: StateEvent<User>) { | |
| const { action, previousState, nextState } = event.detail; | |
| console.log(action, previousState, nextState); | |
| } | |
| userState.subscribe(handleStateChange); | |
| userState.name = 'John Doe'; | |
| userState.trigger('USER_LOADED', { id: 2 }); | |
| userState.unsubscribe(handleStateChange); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment