Skip to content

Instantly share code, notes, and snippets.

@SigurdMW
Created December 14, 2021 06:18
Show Gist options
  • Save SigurdMW/bffd71255cd4ffa6ad253a8f75db8519 to your computer and use it in GitHub Desktop.
Save SigurdMW/bffd71255cd4ffa6ad253a8f75db8519 to your computer and use it in GitHub Desktop.
StencilJS Custom State Provider
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