#VTAH

Implémenter
Redux en TypeScript

PRETEXTE

SUJET

Sara
Ounissi

level 1

Sebastien
Pittion

level 8

wideuxe

Wut?

Redux is a predictable state
container for JavaScript apps.

  • Plutôt bien fashion
  • Inspiré de Flux
  • Une seule source de données
  • Synchrone, immutable
  • Facile à tester
  • Time travel debugging
  • Souvent associé à React, mais pas que...
  • Code plutôt léger mais un peu de boilerplate à l'usage
  • Ne convient pas tout le temps

State

  • Une sorte de BDD en mémoire
  • Comprend les données de l'application
  • C'est un objet dont la structure est libre
     
  • Conseil : le plus à plat possible

Action

  • Le seul moyen de muter le state
  • Une action comprend un type (string)
  • Et optionellement un payload (générique)
  • On dispatch une action sur le store
  • L'action est traitée par les reducers
  • Donc le state varie en fonction des actions

Reducer

  • C'est une fonction pure (sans effet de bord)
  • Donc très facile à tester
  • Qui prend deux paramètres : state et action
  • Traite l'action donnée et retourne un nouveau state
  • L'action est traitée grâce à son type
  • Sinon, l'ancien state est retourné
     
  • Note : combineReducers permet de créer
    un reducer à partir d'une map de reducers

Store

  • C'est l'objet qui relie state, actions et reducers
  • Le store est unique
  • Il expose le state courant via getState
  • Il permet de dispatcher des actions
  • Il appelle les reducers lors du dispatch
  • On peut écouter ses changements via subscribe
  • Chaque dispatch est synchrone

taïpeuscripte

Wut?

TypeScript is a typed superset
of JavaScript that compiles
to plain JavaScript.

  • Plutôt bien fashion aussi
  • Créé par Micro$oft
  • Distribué en tant que module Node.js
  • Facile à installer
  • Facile à configurer
  • Facile à utiliser
  • Basé sur ES6
  • Rajoute du typage
  • Transpile en JavaScript
// tsconfig.json
{
  "compilerOptions": {
    "target": "es2015",
    "module": "es2015",
    "outDir": "./www/",
    "rootDir": "./src/",
    "strict": true,
    "strictNullChecks": false,
    "strictPropertyInitialization": false,
    "esModuleInterop": true
  }
}
npm init
npm install typescript --save-dev
./node_modules/.bin/tsc --init

Installation

Configuration

{
  "private": true,
  "name": "ts-redux",
  "version": "1.0.0",
  "description": "TypeScript Redux implementation",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "concurrently -k -n watch,serve \"npm run watch\" \"npm run serve\"",
    "serve": "lite-server",
    "watch": "tsc --watch"
  },
  "author": "SPI3300",
  "license": "MIT",
  "devDependencies": {
    "concurrently": "3.5.1",
    "lite-server": "2.3.0",
    "typescript": "2.7.2"
  }
}

package.json

Actions

export const enum Actions {
  Init = '[Store] Init'
}

actions.ts

import { Actions } from './actions.js';

export abstract class Action<T> {
  abstract readonly type: Actions;
  constructor(readonly payload: T = null) {}
}

class Init extends Action<void> {
  readonly type: Actions = Actions.Init;
}
export class Action {
  constructor(payload = null) {
    this.payload = payload;
  }
}

class Init extends Action {
  constructor() {
    super(...arguments);
    this.type = "[Store] Init" /* Init */;
  }
}

redux.ts

Reducers

export type State = {
  [key: string]: any;
};

export type Reducer<T, U extends Action<any> = Action<any>> = {
  (state: T, action: U): T;
};

export type Reducers<T extends State> = {
  [K in keyof T]: Reducer<T[K]>;
};

redux.ts

export function combineReducers<T extends State>(reducers: Reducers<T>): Reducer<T> {
  // Use `Object.create(null)` to avoid potential prototypal issues.
  return (oldState: T = Object.create(null), action): T => {
    const newState: T = Object.create(null);
    // Let only changes through to know whether to return new or old state.
    return Object.keys(reducers).filter(key => {
      const oldValue: any = oldState[key];
      const newValue: any = reducers[key](oldValue, action);
      // Store the new value in the new state.
      newState[key] = newValue;
      // Keep only changes in the array.
      return oldValue !== newValue
    // If no changes, return the old state for performance reasons.
    }).length ? newState : oldState;
  };
}
export function combineReducers(reducers) {
  // Use `Object.create(null)` to avoid potential prototypal issues.
  return (oldState = Object.create(null), action) => {
    const newState = Object.create(null);
    // Let only changes through to know whether to return new or old state.
    return Object.keys(reducers).filter(key => {
      const oldValue = oldState[key];
      const newValue = reducers[key](oldValue, action);
      // Store the new value in the new state.
      newState[key] = newValue;
      // Keep only changes in the array.
      return oldValue !== newValue;
    // If no changes, return the old state for performance reasons.
    }).length ? newState : oldState;
  };
}

redux.ts

interface State {
  count: number;
  message: string;
}

const reducersMap: Reducers<State> = {
  count, // Reducer<number>
  message // Reducer<string>
};

combineReducers(reducersMap); // Reducer<State>

Store

export class Store<T extends State> {
  private state: T;
  private readonly emitter: DocumentFragment;
  private readonly event: string = 'dispatch';

  constructor(private readonly reducer: Reducer<T>) {
    // Easy way to handle event listeners.
    this.emitter = document.createDocumentFragment();
    this.dispatch(new Init());
  }

  getState(): T {
    return this.state;
  }

  subscribe(callback: Function): Function {
    // Wrap callback to avoid it to return false or get access to arguments.
    function handler(): void { callback(); }
    this.emitter.addEventListener(this.event, handler);
    return () => { this.emitter.removeEventListener(this.event, handler); };
  }

  dispatch<U extends Action<any>>(action: U): void {
    this.state = this.reducer(this.state, action);
    this.emitter.dispatchEvent(new Event(this.event));
  }
}
export class Store {
  constructor(reducer) {
    this.reducer = reducer;
    this.event = 'dispatch';
    // Easy way to handle event listeners.
    this.emitter = document.createDocumentFragment();
    this.dispatch(new Init());
  }

  getState() {
    return this.state;
  }
  
  subscribe(callback) {
    // Wrap callback to avoid it to return false or get access to arguments.
    function handler() { callback(); }
    this.emitter.addEventListener(this.event, handler);
    return () => { this.emitter.removeEventListener(this.event, handler); };
  }

  dispatch(action) {
    this.state = this.reducer(this.state, action);
    this.emitter.dispatchEvent(new Event(this.event));
  }
}

redux.ts

Exemple

Display :
count + message

Button : -1

Button : +1

Button : -10

Button : +10

DEMO !

Merci ;)

Questions ?

Implémenter Redux en TypeScript

By fingerproof