Slide 1

Slide 1 text

Hello generics, goodbye boilerplate @meeroslav

Slide 2

Slide 2 text

developers love to code @meeroslav

Slide 3

Slide 3 text

developers love to copy/paste code @meeroslav

Slide 4

Slide 4 text

developers love to copy/paste boilerplate code @meeroslav

Slide 5

Slide 5 text

Johnson vs Boilerplate @meeroslav

Slide 6

Slide 6 text

… sections of code that have to be included in many places with little or no alteration … @meeroslav

Slide 7

Slide 7 text

90% @meeroslav

Slide 8

Slide 8 text

Miroslav Jonaš @meeroslav

Slide 9

Slide 9 text

First there was enterprise... @meeroslav

Slide 10

Slide 10 text

Then came the state machine @meeroslav

Slide 11

Slide 11 text

View Reducer Action Store Effect The Redux flow @meeroslav

Slide 12

Slide 12 text

LIVE TUESDAY DECEMBER 1 16:15 on WDLW Most Revealing Trailer Noscar Nominations 2020 Most Unsuitable Score Noscar Nominations 2020 Most Predictable Plot Noscar Nominations 2020 Most Rip-off Scenes Noscar Nominations 2020 @meeroslav NOSCAR Office Nominees History Book Magic Carpet

Slide 13

Slide 13 text

NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW @meeroslav Office Nominees History Book Magic Carpet Load Nominees loading = true

Slide 14

Slide 14 text

NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW LOADING…PLEASE WAIT @meeroslav Office Nominees History Book Magic Carpet loading = false nominees = payload Nominees Loaded Load Nominees loading = true

Slide 15

Slide 15 text

NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW MOST REVEALING TRAILER · THE VAMPIRE STRIKES BACK · SILENCE OF THE LAMPS · LORD OF THE DRINKS · PRESIDENT EVIL · GAME OF DRONES · LIE HARD MOST RIP-OFF SCENES MOST PREDICTABLE PLOT @meeroslav Office Nominees History Book Magic Carpet Load Nominees loading = true loading = false nominees = payload Nominees Loaded

Slide 16

Slide 16 text

NOSCAR NOMINEES NOSCAR LIVE TUESDAY DECEMBER 1 16:15 on WDLW EVEN LIAM NEESON CAN’T FIND THAT @meeroslav Office Nominees History Book Magic Carpet Load Nominees loading = true loading = false error = Error404 Nominees Load Failed

Slide 17

Slide 17 text

export class LoadNominees implements Action { readonly type = NomineeActionTypes.LoadNominees; constructor(public payload: number) {} } export class NomineesLoaded implements Action { readonly type = NomineeActionTypes.NomineesLoaded; constructor(public payload: Nominee[]) {} } export class NomineesLoadFailed implements Action { readonly type = NomineeActionTypes.NomineesLoadFailed; constructor(public payload: number, public error: Error) {} } Action classes @meeroslav

Slide 18

Slide 18 text

Time for generics! @meeroslav

Slide 19

Slide 19 text

Multiple types class Student { firstName: string ; lastName: string ; get name() { return `${this.firstName} ${this.lastName}`; } constructor(firstName: string, lastName: string) { this.firstName = firstName ; this.lastName = lastName ; } } class Car { brand: string ; model: string ; constructor(brand: string, model: string) { this.brand = brand; this.model = model; } } class Pet { name: string ; constructor(name: string) { this.name = name; } } @meeroslav

Slide 20

Slide 20 text

type agnostic function @meeroslav const nameLength = item item.name.length; const student = new Student(‘Jane', ‘Doe'); const dog = new Pet(‘Dobbie'); const suv = new Car('Toyota', 'RAV4'); console.log(nameLength(student), nameLength(dog), nameLength(suv));

Slide 21

Slide 21 text

interface HasName { name: string; } const nameLength = (item: T) item.name.length; const student = new Student('Jane', 'Doe'); const dog = new Pet('Dobbie'); const suv = new Car('Toyota', 'RAV4'); console.log(nameLength(student), nameLength(dog), nameLength(suv)); Controlled type agnostic function @meeroslav const suv: Car Argument of type 'Car' is not assignable to parameter of type 'HasName'. Property 'name' is missing in type 'Car' but required in type 'HasName'.ts(2345)

Slide 22

Slide 22 text

export abstract class BaseAction implements Action { type = null; necessary evil constructor(public payload: T) {} } Generic Action classes @meeroslav

Slide 23

Slide 23 text

Generic Action classes before export class LoadNominees implements Action { readonly type = `[Nominees] Load`; constructor(public payload: number) {} } with generic class export class LoadNominees implements BaseAction { readonly type = `[Nominees] Load`; } with typeless generic class export const LoadNominees implements BaseAction {} } with createAction function export const loadNominees = createAction(`[Nominees] Load`, props<{ year: number }>()); @meeroslav

Slide 24

Slide 24 text

Reducer @meeroslav

Slide 25

Slide 25 text

export function nomineeReducer(state: NomineeState = initialState, action: NomineeActions): NomineeState { switch (action.type) { case NomineeActionTypes.LoadNominees: { return { …state, loading: LoadingState.LOADING, error: null }; } case NomineeActionTypes. NomineesLoaded: { return { …state, loading: LoadingState.SUCCESSFUL,nominees: action.payload }; } case NomineeActionTypes. NomineesLoadFailed: { return { …state, loading: LoadingState. FAILED,error: action.error }; } default: { return state; } } } Reducer @meeroslav

Slide 26

Slide 26 text

const featureReducer = createReducer( initialState, on(loadNominees, (state) ({ …state, loading: LoadingState.LOADING, error: null })), on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees })), on(nomineesLoadFailed, (state, { error }) ({ …state, loading: LoadingState.FAILED, error })) ); export function nomineeReducer(state: NomineeState, action: Action) { return featureReducer(state, action); } Reducer with functions @meeroslav

Slide 27

Slide 27 text

Grouped Actions creators export const loadNominees = createAction(`${prefix} Load`, props<{ year: number, actionGroup: ActionGroup.LOAD }>()); export const nomineesLoaded = createAction(`${prefix} Success`, props<{ nominees: Nominee [], actionGroup: ActionGroup.SUCCESS }>()); export const loadNomineesFailed = createAction(`${prefix} Load Failed`, props<{ year: number, error: Error, actionGroup: ActionGroup.FAILURE }>()); actions const myAction = loadNominees({ year: 2020, actionGroup: ActionGroup.LOAD }); dedicated creator factories export const loadNominees = createLoadAction(`${prefix} Load`, props<{ year : number }>()); export const nomineesLoaded = createSuccessAction(`${prefix} Success`, props<{ nominees: Nominee[] }>()); export const loadNomineesFailed = createFailureAction(`${prefix} Load Failed`, props<{ year: number, error: Error }>()); new usage const myAction = loadNominees({ year: 2020 }); @meeroslav

Slide 28

Slide 28 text

Grouped Reducer standard way const featureReducer = createReducer ( initialState, on(loadNominees, (state) ({ …state, loading: LoadingState. LOADING, error: null })), on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees })), on(nomineesLoadFailed, (state, { error }) ({ …state, loading: LoadingState.FAILED, error })) ); export function nomineeReducer(state: NomineeState, action: Action): NomineeState { return featureReducer(state, action); } grouped reducer const successReducer = createReducer ( initialState, on(nomineesLoaded, (state, { nominees }) ({ …state, loading: LoadingState.SUCCESSFUL, nominees }))); export function nomineeReducer(state: NomineeState, action: Action): NomineeState { return createGroupedReducer(initialState, actions, { successReducer })(state, action); } @meeroslav

Slide 29

Slide 29 text

export function createGroupedReducer( initialState: S, creators: { [key: string]: ActionCreator } , config GroupedReducers ): ActionReducer { const creatorTypes: Record = Object.keys(creators) .reduce((acc, cur) ({ …acc, [creators[cur].type]: cur }), {}); return (state: S = initialState, action: A ): S { if (!creatorTypes[action.type]) { return state; } if (isGroupedAction(action)) { if (action.actionGroup ActionGroup.LOAD) { return config.loadReducer ? config.loadReducer(state, action) : defaultLoadReducer(state); } if (action.actionGroup ActionGroup.SUCCESS) { return config.successReducer ? config.successReducer(state, action) : defaultSuccessReducer(state); } if (action.actionGroup ActionGroup.FAILURE) { return config.failureReducer ? config.failureReducer(state, action) : defaultFailedReducer(state, action); } throw new Error(`Unexpected action group ${action.actionGroup}.`); } return config.generalReducer ? config.generalReducer(state, action) : defaultGeneralReducer(state); }; } Grouped Reducer under the hood @meeroslav

Slide 30

Slide 30 text

Effects @meeroslav

Slide 31

Slide 31 text

Effects @Injectable() export class NomineeEffects { loadNominees$ = createEffect(() this.actions$ .pipe( ofType(loadNominees), switchMap(({ type, …payload }) this.service.getNominees({ type, …payload }) .pipe( map(nominees nomineesLoaded({ nominees })), catchError(error of(nomineesLoadFailed({ error, …payload }))) ) ) )); constructor(private readonly actions$: Actions, private readonly service: Nominee Service ){} } @meeroslav

Slide 32

Slide 32 text

Effects @Injectable() export class NomineeEffects { loadNominees$ = this.createSwitchMapEffect( loadNominees, this.service.getNominees, () map(nominees nomineesLoaded({ nominees })), nomineesLoadFailed ); constructor(private readonly actions$: Actions, private readonly service: Nominee Service ){} } @meeroslav

Slide 33

Slide 33 text

protected createConcatMapEffect, RES = unknown>( allowedTypes: TypeOrCreator | [TypeOrCreator, …Array], method: (action: Omit) Observable, innerPipe: (action: Action & REQ) OperatorFunction, failureTarget: ActionCreator ): Observable { return createEffect(() this.actions$.pipe( ofType(…Array.isArray(allowedTypes) ? allowedTypes : [allowedTypes]), concatMap(({ type, …request }: Action & REQ) method(request) .pipe( this.mapInnerPipe(innerPipe, failureTarget, { type, …request } as Action & REQ)) ) )); } private mapInnerPipe( innerPipe: (action: REQ) OperatorFunction, failureTarget: ActionCreator, action: Action & REQ ): OperatorFunction { const { type, actionGroup, …payload } = action as GroupedAction; return pipe.call(this, innerPipe(action), catchError(error of(failureTarget({ …payload, error }))) ); } Inside the Machine @meeroslav

Slide 34

Slide 34 text

Generics all the things! @meeroslav

Slide 35

Slide 35 text

export abstract class Pageable { items: Array = []; skip = 0 ; private itemsCount: number; get hasMoreItems(): boolean { return this.itemsCount > PAGE_LIMIT; } get page(): number { return this.skip / PAGE_LIMIT + 1; } constructor (items: T[], skip: number = 0) { this.itemsCount = items.length; this.items = items.slice(0, PAGE_LIMIT); this.skip = skip; } } usage export class Nominees extends Pageable { } Pageable collection @meeroslav This is our wrapper for paginable collections. The actual implementation has more juice but in a nutshell we use generics to add common functionality to different types.

Slide 36

Slide 36 text

Different Set of glasses @meeroslav

Slide 37

Slide 37 text

Goodbye playlist Krewella - Say Goodbye Andrea Bocelli - Time to say goodbye (Con te partiro) Apparat - Goodbye Placebo - Song to say goodbye Green Day - Say goodbye Cage the Elephant - Goodbye Madsen - Goodbye Logik The Hoosiers - Goodbye Mr A Auf Wiedersehen - Zdravo Dovidjenja My Fearless Friend - Goodbye bit.ly/goodbye - boilerplate @meeroslav

Slide 38

Slide 38 text

@meeroslav bit.ly/goodbye - boilerplate Stay Safe Stay Healthy Use Generics