Skip to content

Instantly share code, notes, and snippets.

@ali-master
Created July 9, 2021 19:12
Show Gist options
  • Save ali-master/b86857b0a2ec196ed5a0faf2f3b9404e to your computer and use it in GitHub Desktop.
Save ali-master/b86857b0a2ec196ed5a0faf2f3b9404e to your computer and use it in GitHub Desktop.
A simple redux from scratch
/**
* Redux
* - getState()
* - subscribe()
* - dispatch() -> ui -> state
* - combineReducer
*
*
* - replaceReducer
* - injectReducer
*
*/
interface Action {
type: string;
};
interface AnyAction extends Action {
[extraProps: string]: any;
}
interface Dispatch<A extends Action = AnyAction> {
<T extends A>(action: T): T;
}
type Listener = () => void;
interface Unsubscribe {
(): void;
}
interface Store<S = any, A extends Action = AnyAction> {
getState(): S;
subscribe(listener: Listener): Unsubscribe;
dispatch: Dispatch<A>;
}
type Reducer<S = any, A extends Action = AnyAction> = (state: S, action: A) => S;
type PreloadedState<S> = Required<S>;
function createStore<S, A extends Action = AnyAction>(reducer: Reducer<S, A>, preloadedState?: PreloadedState<S>): Store<S, A> {
if(typeof reducer !== "function") {
throw new Error("Reducer should be a function");
}
if(typeof preloadedState === "function") {
throw new Error("preloadedState can't be a function");
}
let currentState = preloadedState as S;
let currentReducer = reducer;
let currentListeners: Array<Listener> = [];
let nextListeners = currentListeners;
let isDispatching = false;
function getState(): S {
if(isDispatching) {
throw new Error(`You may not call store.getState() while the reducer is executing.`);
}
return currentState;
}
function subscribe(listener: Listener): Unsubscribe {
if(typeof listener !== "function") {
throw new Error("Expected the listener to be a function. Instead, received " + kindOf(listener));
}
if(isDispatching) {
throw new Error("You may not call store.subscribe() while the reducer is executing.");
}
nextListeners.push(listener);
return function unsubscribe() {
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
currentListeners = [];
}
}
function dispatch(action: A): void {
if(!isPlainObject(action)) {
throw new Error(`Actions must be plain objects. Instead, the action type was ${kindOf(action)}`);
}
if(!action.hasOwnProperty("type")) {
throw new Error(`Actions may not have an undefined "type" property. You may misspelled an action type string constant`);
}
if(isDispatching) {
throw new Error("Reducers may not dispatch action, because they are busy.");
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
}finally {
isDispatching = false;
}
notifyListeners();
}
function notifyListeners(): void {
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
}
dispatch({type: "@INIT"} as A);
return {
getState,
subscribe,
dispatch: dispatch as Dispatch<A>,
}
};
declare const $CombinedState: unique symbol;
type ReducersMapObject<S = any, A extends Action = AnyAction> = {
[K in keyof S]: Reducer<S[K], A>;
}
type CombinedState<S> = { readonly [$CombinedState]?: undefined } & S;
type StateFromReducerMapObject<M> = M extends ReducersMapObject ? {
[P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never
} : never;
function combineReducers<S>(
reducers: ReducersMapObject<S, any>
): Reducer<CombinedState<S>>;
function combineReducers<S, A extends Action = AnyAction>(
reducers: ReducersMapObject<S, A>
): Reducer<CombinedState<S>, A>;
function combineReducers(reducers: ReducersMapObject) {
const reducerKeys = Object.keys(reducers);
const finalReducers: ReducersMapObject = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if(process.env.NODE_ENV !== "production" && typeof reducers[key] !== "function") {
throw new Error(`No reducer provided for key ${key}`);
}
if(typeof reducers[key] === "function") {
finalReducers[key] = reducers[key];
}
}
const finalReducersKeys = Object.keys(finalReducers);
let shapeAssertionsError: Error;
try {
assertReducerShape(finalReducers);
}catch (e) {
shapeAssertionsError = e;
}
return function combination(state: StateFromReducerMapObject<typeof reducers> = {}, action: AnyAction) {
if(shapeAssertionsError) {
throw shapeAssertionsError;
}
let hasChanged = false;
const nextState: StateFromReducerMapObject<typeof reducers> = {};
for (let i = 0; i < finalReducersKeys.length; i++) {
const key = finalReducersKeys[i];
const reducer = finalReducers[key];
const previousStateForKey = state?.[key];
const nextStateForKey = reducer(previousStateForKey, action);
if(typeof nextStateForKey === "undefined") {
const actionType = action?.type;
throw new Error(`When called an action of type ${actionType ? String(actionType) : "(unknown type)"}, the slice reducer for key ${key} returned undefined. To ignore an action, you must explicitly return the previous state.`)
}
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
hasChanged = hasChanged || finalReducersKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
}
}
function assertReducerShape(reducers: ReducersMapObject) {
Object.entries(reducers).forEach(([key, reducer]) => {
const initialState = reducer(undefined, {type: "@INIT"});
if(typeof initialState === "undefined") {
throw new Error(`The slice reducer for key ${key} returned undefined during initialization.`);
}
const randomActionType = Math.random().toString(16).slice(2);
const $initialStateWidthRandomActionType = reducer(undefined, {type: randomActionType});
if(typeof $initialStateWidthRandomActionType === "undefined") {
throw new Error(`The slice reducer for key ${key} returned undefined probed with a random action type`);
}
})
}
function kindOf(inp: any): string {
return Object.prototype.toString.call(inp).slice(8, -1).toLowerCase();
}
function isPlainObject(inp: any): boolean {
return kindOf(inp) === "object";
}
interface PersonReducer {
name: string;
family: string;
}
const initialPersonState: PersonReducer = {
name: "Ali",
family: "Torki"
}
const personReducer: Reducer<PersonReducer> = (state = initialPersonState, action) => {
switch (action.type) {
case "UPDATE":
return {
name: action.payload.name,
family: action.payload.family
};
default:
return state;
}
}
interface CounterReducer {
value: number;
}
const initialCounterReducer: CounterReducer = {
value: 0,
}
const counterReducer: Reducer<CounterReducer> = (state = initialCounterReducer, action) => {
switch (action.type) {
case "INC":
return {
value: action.payload.value,
};
default:
return state;
}
}
interface StateNetwork {
person: PersonReducer,
counter: CounterReducer
}
const store = createStore<StateNetwork>(combineReducers({
person: personReducer,
counter: counterReducer
}));
console.log(store.getState().person.name);
const unwatch = store.subscribe(() => {
console.log("changes", store.getState())
})
store.dispatch({
type: "INC",
payload: {
value: 11
}
})
// unwatch();
store.dispatch({
type: "UPDATE",
payload: {
name: "Test Name",
family: "Test Family"
}
})
console.log(store.getState().person.name);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment