Last active
November 9, 2019 22:07
-
-
Save jakobz/047b52bccc7d19bb24904fe5e981aeef to your computer and use it in GitHub Desktop.
Typed Redux wrapper
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 { Reducer } from "redux"; | |
| import { Dispatch } from "react-redux"; | |
| export type ReducerCur<TState, TPayload = any> = (payloyoad: TPayload) => (state: TState) => TState; | |
| export type ActionCreator<TPayload> = (payload: TPayload) => ({ type: string } & TPayload); | |
| type GetActionCreator<TState, T> = T extends ReducerCur<TState, infer TPayload> ? ActionCreator<TPayload> : void; | |
| export type ActionCreatorsSet<TState, T> = { [TName in keyof T]: GetActionCreator<TState, T[TName]> }; | |
| export interface Bundle<TState, TReducersSet> { | |
| reducer: Reducer<TState, any>; | |
| actionCreators: ActionCreatorsSet<TState, TReducersSet>; | |
| bind(dispatch: Dispatch<TState>): ActionCreatorsSet<TState, TReducersSet>; | |
| } | |
| export interface BuildReducerOptions { | |
| prefix?: string; | |
| } | |
| export function buildReducer<TState, TReducersSet>( | |
| initialState: TState, | |
| reducersSet: TReducersSet, | |
| options?: BuildReducerOptions | |
| ): Bundle<TState, TReducersSet> { | |
| const effectiveOptions: BuildReducerOptions = options || {}; | |
| const reducers: Record<string, ReducerCur<TState>> = {}; | |
| const actionCreators: any = {}; | |
| const actionDispatchers: any = {}; | |
| Object.keys(reducersSet).forEach((name: string) => { | |
| let type = name.replace(/(?:^|\.?)([A-Z])/g, (x, y) => { return "_" + y.toLowerCase(); }).replace(/^_/, "").toUpperCase(); | |
| if (effectiveOptions.prefix) { | |
| type = effectiveOptions.prefix + '_' + type; | |
| } | |
| reducers[type] = (reducersSet as any)[name]; | |
| actionCreators[name] = (payload: any) => ({ type, ...payload }); | |
| actionDispatchers[name] = (dispatch: Dispatch<TState>) => (payload: any) => dispatch({ ...payload, type }); | |
| }); | |
| function reducer(state: any, action: any) { | |
| if (typeof state === 'undefined') { | |
| return initialState; | |
| } | |
| const reducerCar: ReducerCur<any, any> = (reducers as any)[action.type]; | |
| const newState = reducerCar ? reducerCar(action)(state) : state; | |
| return newState; | |
| } | |
| function bind(dispatch: Dispatch<TState>) { | |
| const actionEmitters: any = {}; | |
| Object.keys(actionDispatchers).forEach(key => actionEmitters[key] = actionDispatchers[key](dispatch)); | |
| return actionEmitters; | |
| } | |
| return { reducer, actionCreators, bind }; | |
| } | |
| // usage in another file | |
| import { buildReducer, ReducerCur } from '../reduxHelpers'; | |
| import { SupportState, SupportTabs, SupportErrors } from './supportModel'; | |
| import * as models from '../../../server/models'; | |
| import { ImageFile } from 'react-dropzone'; | |
| import { uniqueArray } from '../../common/index'; | |
| const initialState: SupportState = { | |
| inputValue: '', | |
| dropzoneActive: false, | |
| files: [], | |
| answers: [], | |
| isSearching: false, | |
| isError: false, | |
| currentTab: SupportTabs.main, | |
| selectedAnswer: null, | |
| validationErrors: [], | |
| tryAgainHandler: () => { }, | |
| attemptsTryAgainCounter: 0, | |
| thanksMessage: '', | |
| }; | |
| const input = (a: { inputValue: string }) => (s: SupportState) => ({ ...s, inputValue: a.inputValue }); | |
| const clearInput = ({ }) => input({ inputValue: '' }); | |
| const setDropzoneStatus = (a: { isActive: boolean }) => (s: SupportState) => ({ ...s, dropzoneActive: a.isActive }); | |
| const dropzoneIsActive = ({ }) => setDropzoneStatus({ isActive: true }); | |
| const dropzoneIsNotActive = ({ }) => setDropzoneStatus({ isActive: false }); | |
| const addFiles = (a: { files: ImageFile[] }) => (s: SupportState) => ({ ...s, files: [...a.files, ...s.files], dropzoneActive: false }); | |
| const removeFile = (a: { file: ImageFile }) => (s: SupportState) => ({ ...s, files: s.files.filter(f => f !== a.file) }); | |
| const resetForm = ({ }) => (s: SupportState) => ( | |
| { ...s, files: [], inputValue: '', answers: [], dropzoneActive: false, errors: [], isSearching: false, isError: false, attemptsTryAgainCounter: 0 } | |
| ); | |
| const cleanErrors = ({ }) => (s: SupportState) => ({ ...s, errors: [], isError: false}); | |
| const startSearching = ({ }) => (s: SupportState) => ({ ...s, isSearching: true, isError: false }); | |
| const receiveKBAnswers = (a: { answers: models.KbAnswer[] }) => (s: SupportState) => ({ ...s, answers: a.answers, isSearching: false, attemptsTryAgainCounter: 0 }); | |
| // .... | |
| const bundle = buildReducer( | |
| initialState, | |
| { | |
| input, clearInput, dropzoneIsActive, dropzoneIsNotActive, addFiles, removeFile, resetForm, | |
| startSearching, receiveKBAnswers, showMainTab, showAnswerTab, showErrorTab, showSuccessTab, | |
| addError, removeError, setTryAgainHandler, cleanErrors, setThanksMessage | |
| }, | |
| { prefix: "SUPPORT" } | |
| ); | |
| export const supportActions = bundle.actionCreators; | |
| export const supportBindActions = bundle.bind; | |
| export const supportReducer = bundle.reducer; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment