Created
December 14, 2021 06:18
-
-
Save SigurdMW/bffd71255cd4ffa6ad253a8f75db8519 to your computer and use it in GitHub Desktop.
StencilJS Custom State Provider
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
import { forceUpdate } from '@stencil/core'; | |
interface Todo { | |
completed: boolean; | |
name: string; | |
date: Date; | |
id: string; | |
} | |
interface State { | |
some: string; | |
count: number; | |
todos: Todo[]; | |
} | |
type GenericAction<T, P> = { type: T; payload: P }; | |
type AllActions = GenericAction<'SOME_ACTION', string> | GenericAction<'COUNT', number> | GenericAction<'TODO', string>; | |
const idGenerator = () => { | |
let currentId = 0; | |
return () => { | |
currentId += 1; | |
return currentId + Date.now() + ''; | |
}; | |
}; | |
function diff<T>(oldState: T, newState: T): Array<keyof State> { | |
const changedKeys: Array<keyof State> = []; | |
Object.keys(newState).forEach(key => { | |
if (newState[key] !== oldState[key]) { | |
changedKeys.push(key as any); | |
} | |
}); | |
return changedKeys; | |
} | |
const generator = idGenerator(); | |
type ReducerType<S, A> = (s: S, action: A) => S; | |
const reducer = (state: State, action: AllActions): State => { | |
switch (action.type) { | |
case 'SOME_ACTION': | |
return { ...state, some: action.payload }; | |
case 'COUNT': | |
return { ...state, count: action.payload }; | |
case 'TODO': | |
const index = state.todos.findIndex(todo => todo.id === action.payload); | |
if (index === -1) return state; | |
const newTodos = [...state.todos]; | |
newTodos[index].completed = !newTodos[index].completed; | |
return { | |
...state, | |
todos: newTodos, | |
}; | |
default: | |
return state; | |
} | |
}; | |
function subscribableStore<S extends object, A>(initialState: S, reducer: ReducerType<S, A>) { | |
let state: S = { ...initialState }; | |
/** List of component ids and their render method */ | |
const subscriptions: { [id: string]: Function } = {}; | |
/** Mapping between keys in state and what component id is subscribed to it */ | |
const keys: { [key in keyof S]?: string[] } = {}; | |
/** | |
* Function responsible for running the render method of the | |
* components when the keys in state they | |
* subscribe to change | |
* @param changedKeys keys in state that are changed | |
*/ | |
const runSubscriptionsForChangedKeys = (changedKeys: Array<keyof State>) => { | |
const idsToRun: string[] = []; | |
changedKeys.forEach(key => { | |
if (keys.hasOwnProperty(key)) { | |
keys[key].forEach(id => { | |
if (!idsToRun.includes(id)) { | |
idsToRun.push(id); | |
} | |
}); | |
} | |
}); | |
idsToRun.forEach(id => { | |
if (subscriptions.hasOwnProperty(id)) { | |
subscriptions[id](); | |
} | |
}); | |
}; | |
/** Get the global state */ | |
const getState = () => state; | |
/** Dispatching actions is the only way to update state */ | |
const dispatch = (action: A) => { | |
const newState = reducer(state, action); | |
const changedKeys = diff(state, newState); | |
state = newState; | |
runSubscriptionsForChangedKeys(changedKeys); | |
}; | |
/** | |
* Function responsible for adding the relationship between a key in state | |
* and the component that subscribe to it | |
* @param subscribeKeys keys in state to subscribe the component to | |
* @param id id of the component subscribing | |
*/ | |
const addIdToKeys = (subscribeKeys: Array<keyof S>, id: string) => { | |
subscribeKeys.forEach(key => { | |
if (keys.hasOwnProperty(key)) { | |
keys[key].push(id); | |
} else { | |
keys[key] = [id]; | |
} | |
}); | |
}; | |
/** | |
* Function to clean up the subscription when a component disconnects | |
* @param id id of the component subscribing | |
*/ | |
const removeIdFromKeys = (id: string) => { | |
Object.keys(keys).map(key => { | |
if (keys[key].includes(id)) { | |
const newIds = keys[key].filter(i => i !== id); | |
if (newIds.length === 0) { | |
delete keys[key]; | |
} else { | |
keys[key] = newIds; | |
} | |
} | |
}); | |
}; | |
return { | |
/** | |
* Subscribe a component to specific keys in state so that we can render that | |
* component only when the relevant state changes | |
* @param keys the keys in the store you want the component to subscribe to | |
* @param that this | |
* @returns void | |
* @example | |
* componentDidLoad = () => { | |
* this.subscriptionId = store.addSubscription(['some', 'count'], this); | |
* }; | |
*/ | |
addSubscription: (keys: Array<keyof S>, that: any) => { | |
if (!that || !('render' in that)) { | |
console.warn("The store's `addSubscription` method must have a 2nd argument `this`. `this` must have a `render` method. See example."); | |
return; | |
} | |
const id = generator(); | |
subscriptions[id] = () => forceUpdate(that); | |
addIdToKeys(keys, id); | |
return id; | |
}, | |
/** | |
* Remove subscription when the component disconnects | |
* @param id is of the subscribed component | |
* @returns void | |
* @example | |
* disconnectedCallback() { | |
* store.removeSubscription(this.subscriptionId); | |
* } | |
*/ | |
removeSubscription: (id: string) => { | |
if (!id) { | |
console.warn('Whoops, no id received in call to `removeSubscription`'); | |
return; | |
} | |
delete subscriptions[id]; | |
removeIdFromKeys(id); | |
}, | |
getState, | |
dispatch, | |
}; | |
} | |
const initialState: State = { | |
some: '', | |
count: 0, | |
todos: [{ id: '1', completed: false, date: new Date(), name: 'State mngt' }], | |
}; | |
export const store = subscribableStore<State, AllActions>(initialState, reducer); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment