}
```
Sinon une CSS globale, ou une CSS par composant (Webpack powered)
```typescript
let css = require('./mycomponent.css');
```
---
#React-bootstrap
Adaptation des composants bootstrap à React
```typescript
```
La documentation
* [https://react-bootstrap.github.io/components.html](https://react-bootstrap.github.io/components.html)
* [http://getbootstrap.com/components/](http://getbootstrap.com/components/)
---
template: default
layout: true
### Tips
---
# Caractères spéciaux
```typescript
Jet d{String.fromCharCode(39)}eau
```
---
# JSX if (else)
if
```typescript
{this.state.user &&
{this.state.user.login}
}
```
not
```typescript
{this.state.user ||
No User
}
```
if-else
```typescript
{this.state.user ?
{this.state.user.login}
:
No User
}
```
---
# Children
```typescript
interface Props {
readonly title: string;
}
export class ButtonApp extends React.Component
{
render() {
return
}
}
```
```typescript
interface ButtonProps {
readonly title: string;
readonly children?: any;
}
export const Button = ({title, children}: ButtonProps) => (
);
```
---
layout: false
#TP-03
## Liste des étudiants
1. Installer react-bootstrap et ses définitions de type
2. Afficher un champ texte qui permet de filtrer les éléments d'un tableau
3. Afficher le nom de l'étudiant sur lequel on a cliqué, ou un message si aucun n'est sélectionné
4. Bonus ES6: Calculer de façon élégante la moyenne des notes d'un élève (reduce)
---
# Rappels
* Un composant a des propriétés et des états
* Un composant stateless est plus maintenable et testable
* On ne peut passer des propriétés qu'aux enfants (ce n'est pas de l'héritage;) )
---
layout: false
class: center, middle, inverse
# Flux & Redux
---
template: default
layout: true
### Flux & Redux
---
# Flux
* Store: Stocker la donnée + Logique
* Action: Modifier le modèle
* Dispatcher: Répartir les actions dans les stores
* View: Les composants UI
![flux acton diagram](images/flux.png)
**Le flux ne va que dans un seul sens !** (~~2-way binding~~)
---
# Bien / Pas bien
![best practices](images/flux-bestpractice.png)
* On ne met pas à jour directement les autres composants
* On passe par le store (redux), les stores (flux), les observables (RxJs), ...
---
# Redux
* Un store unique (pratique pour l'isomorphisme)
* L'état du store est read-only
* On émet des *actions* pour modifier l'état
* Les actions sont interprétées par des *reducers*
* les *reducers* sont des fonctions *pures*
* les *reducers* prennent en entrée l'état précédent et une action
* ils retournent le nouvel état dans un objet immutable
![redux](images/redux.png)
---
# Actions
### Actions
```typescript
{type: 'INCREMENT', inc: 3}
{type: 'ADD_TODO', text: 'Acheter du pain'}
{type: 'SET_USER', user: new User(1, 'toto')}
```
### Interface
```typescript
export interface CounterAction {
type: string;
inc: number;
}
```
### Action Creator
```typescript
export const incrementor = (inc: number): CounterAction => {
return {type: 'INCREMENT', inc: inc};
};
```
![redux](images/redux.png)
---
# Reducer
```typescript
export const display =
(state: number = 0, action: CounterAction) => {
switch (action.type) {
case 'INCREMENT':
return state + action.inc;
case 'DECREMENT':
return state - action.inc;
default:
return state;
}
};
```
Le *state* est en read-only !
![redux](images/redux.png)
---
# Store
```typescript
interface Store {
dispatch: Dispatch;
getState(): S;
subscribe(listener: () => void): Unsubscribe;
replaceReducer(nextReducer: Reducer): void;
}
```
* Le *store* garde l'état de l'application. On y accède via *getState()*.
* ~~setState()~~ !! On modifie l'état en 'dispatchant' une action: *dispatch(filterWarnings(true))*
* On peut être prévenu des changements du store via *subscribe(listener)*
* On se désenregistre avec la méthode retournée par *subscribe*
![redux](images/redux.png)
---
layout: false
class: center, middle, inverse
# React & Redux
---
template: default
layout: true
### React & Redux
---
# Composition de reducers - Approche naïve
```typescript
interface State = {readonly user: string; readonly counter: number;};
const initialState = {user: '', counter: 0};
export const myreducer =
(state = initialState, action: any) => {
switch (action.type) {
case 'SET_USER':
return Object.assign({}, state, {user: action.user});
case 'INCREMENT':
return Object.assign({}, state, {counter: state.counter + 1});
case 'DECREMENT':
return Object.assign({}, state, {counter: state.counter - 1});
default:
return state;
}
};
```
Ca peut faire lourd...!
---
# Composition de reducers - Découpage
```typescript
export const user =
(state: string = '', action: UserAction) => {
switch (action.type) {
case 'SET_USER':
return action.user;
default:
return state;
}
};
export const counter =
(state: number = 0, action: CounterAction) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};
```
---
# Composition de reducers - Effet Waouh
```typescript
interface State = {readonly user: string; readonly counter: number;};
const initialState = {user: 'nobody', counter: 0};
export const myreducer =
(state = initialState, action: any) => {
return {
user: user(state.user, action),
counter: counter(state.counter, action)
}
};
```
Et avec un peu de magie...
```typescript
import { combineReducers } from 'redux'
export const myreducer = combineReducers({
user,
counter
});
```
---
# Récapitulatif
- Actions
- Reducers
- Store
- Composants React
- Il reste à lier les composants au store !
![redux](images/redux.png)
---
# Présentation et Conteneur
* Les composants stateless sont simples à maintenir
* Séparation des responsabilités
![react-redux](images/conteneur-pres.png)
---
# Présentation et Conteneur
** On découpe nos composants (connectés au *store*) en 2, *présentation* & *conteneur* **
| | Presentational Components | Container Components |
| ------------- |-----------------------------------| ----------------------------------------------|
| Purpose |How things look (markup, styles) | How things work (data fetching, state updates)|
| Aware of Redux| No | Yes |
| To read data | Read data from props | Subscribe to Redux state |
| To change data| Invoke callbacks from props | Dispatch Redux actions |
| Are written | By hand | Usually generated by React Redux |
---
# Connect
* Le composant de présentation est comme les composants simples rencontrés jusque la (sans appels ajax)
* Le composant conteneur va être généré automatiquement, ** mais **
* il faut décrire comment créer les *props* à partir des données du *store*: **mapStateToProps(state: State): StateProps {...}**
* il faut implémenter les callbacks qui seront injectés dans les *props*: **mapDispatchToProps(dispatch: Function): DispatchProps {...}**
```typescript
import {connect} from 'react-redux';
export const MyComponent =
connect(mapStateToProps, mapDispatchToProps)(MyPresentationalComponent);
```
---
## Composant de présentation:
```typescript
export const SelectCodePostal_ = ({cps, currentCp, onSelectCp}: Props) => (
{
cps.map((cp: number) => )
}
);
```
---
## Conteneur
```typescript
interface StateProps {
readonly cps: ReadonlyArray;
readonly currentCp: number;
}
interface DispatchProps {
onSelectCp: (e: Event) => void;
}
type Props = StateProps & DispatchProps;
const mapStateToProps = (state: State): StateProps => {
const cpsOfAgence = state.codesPostaux
.map((cp: CodePostal) => cp.id);
return {cps: cpsOfAgence, currentCp: state.codePostal.id};
};
const mapDispatchToProps = (dispatch: Function): DispatchProps => {
return {
onSelectCp: (e: Event) => {
let codePostalId =
new CodePostalId(parseInt((e.target as any).value));
dispatch(setCodePostalId(codePostalId));
}
};
};
export const SelectCodePostal =
connect(mapStateToProps, mapDispatchToProps)(SelectCodePostal_);
```
---
# Passer le store
Tous les conteneurs de l'application doivent avoir accès au store.
```typescript
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import {reducer, State} from './reducers/index';
import {App} from './components/app';
const store = createStore(reducer);
ReactDOM.render(
,
document.getElementById('app')
);
```
---
layout: false
#TP-04
## Liste de courses - bis
1. Installer les packages redux et react-redux, ainsi que leurs '@types'
2. Reprendre le TP-02 mais en utilisant le *state* du store plutot que le *state* du composant,
et en initialisant la liste sans chargement asynchrone (liste en dur).
* Créer l'interface **State** du *state* du *store* (la liste d'items)
* Creer une interface **ItemsAction**
* Créer un *action creator* **setItems** qui renvoie une **ItemAction**
* Créer un reducer **items** qui prend une **ItemAction** en entrée
* Créer un reducer global à l'aide de **combineReducer**
* Créer le *contener* et le *presentational component*
* Passer un titre dans les ownProps (et donc créer l'interface **OwnProps**)
3. Bonus: Créer un bouton qui permet d'ajouter des items au store.
---
template: default
layout: true
### Actions asynchrones
---
## Généralités
* Redux ne permet que de dispatcher des objets
* **redux-thunk** permet de passer une fonction, et donc des actions asynchrones ou des actions avec conditions.
C'est un **middleware**.
* Avec **TypeScript** on se fait quelques noeuds au cerveau ;)
* C'est plus élégant que les actions retournent des **promesses**
---
## Middleware
```typescript
import {reducer} from './reducers/index';
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
reducer,
applyMiddleware(thunk)
);
```
---
## Promesses
```typescript
const getItemsId: (() => Promise) =
() => axios.get- ('src/items.json')
.then(resp => resp.data)
.then((items: Item[]) => items.map(item => item.id))
.catch((error: any) => console.log(error.toString()))
```
---
## Sans TypeScript
```javascript
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function incrementCreator(inc) {
return {
type: INCREMENT_COUNTER,
inc: inc
};
}
function incrementAsyncCreator(inc) {
return dispatch => {
setTimeout(() => {
dispatch(incrementCreator(inc));
}, 1000);
};
}
```
---
## Avec TypeScript
Une **ThunkAction** est une fonction qui reçoit en entrée les méthodes *dispatch()* et *getState()*, et retourne le
résultat de **dispatch()**
```typescript
export type ThunkAction =
(dispatch: Dispatch
, getState: () => S, extraArgument: E) => R;
export interface Dispatch {
(asyncAction: ThunkAction): R;
}
```
Dans le code, on va écrire des **ThunkActionCreator**
```typescript
type ThunkAction2 = ThunkAction;
export type ThunkActionCreator = (...args: any[]) => ThunkAction2;
// Ou en une ligne
export type ThunkActionCreator = (...args: any[]) => ThunkAction;
```
```typescript
export const fetchItems: ThunkActionCreator = () =>
(dispatch, getState) => getItems(getState().userId)
.then(items => {
dispatch(setItems(items));
})
.catch((error: any) => {
console.log(error);
});
```
---
layout: false
#TP-05
## Liste de courses - ter
1. Installer redux-thunk et le '@types'
2. Reprendre le TP-04 mais faisant un appel asynchrone (http) pour charger la liste d'items
3. Bonus: Faire une action qui avant d'ajouter l'item au store, applique la TVA sur le prix lors d'un ajout d'item par le formulaire
---
# Bravo !
* C'était la partie la plus compliquée
* On sait créer des compostants et leur passer des propriétés
* Depuis leur composant parent
* Depuis depuis un l'état 'local' (**LocalState**)
* Depuis l'état du **store** (**State**)
* Depuis un composant (grand)n-parent par le context... ou pas !
---
template: default
layout: true
### Context
---
## Mises en garde
* On peut passer des objets à ses composants enfants (n'importe où dans la hiérarchie).
* Des librairies sont basées la dessus, mais ce n'est pas officiellement supporté (le fonctionnement pourra varier, être supprimé).
La doc par d'**experimental API** !
* *react-redux* fonctionne grâce à ca (le store est passé à tous les enfants).
* ** A n'utiliser qu'en cas d'urgence ;)**
---
## PropTypes
* Nécessaires pour faire fonctionner le *context*
* Permet de décrire (typer, rendre obligatoire, ...) des propriétés
* Pas vraiment utile quand on fait du typescript !
---
## Utilisation
Dans le composant parent, il faut implémenter *getChildContext()*
```typescript
getChildContext(): MyContext {
return {color: 'purple', user: new User(0, 'admin')};
}
```
Puis définir les *childContextTypes*, toujours dans le composant parent
```typescript
static childContextTypes = {
color: React.PropTypes.string.isRequired,
user: React.PropTypes.object.isRequired
};
```
---
## Utilisation
Dans le composant enfant, on peut typer le *context*, il faut déclarer les *contextTypes*, et là on
a accès au context dans le composant.
```typescript
class Child extends React.Component {
context: MyContext;
static contextTypes = {
color: React.PropTypes.string.isRequired,
user: React.PropTypes.object.isRequired
};
render() {
return user = {this.context.user.login}
;
}
}
```
---
layout: false
#TP-06
## Contexte
Le but est de passer une couleur et un **User** d'un composant à son composant 'petit-fils'
1. Déclarer un context dans le composant grand parent (Large), avec une couleur et un User.
2. Afficher les informations passées dans le *context* dans le composant petit-fils (Small)
---
template: default
layout: true
### Tests
---
## Jest & Enzyme
L'idée est de tester les composants de *présentation*, avec leurs états et leurs propriétés.
[Jest API](https://facebook.github.io/jest/docs/api.html#content)
[Enzyme API](https://github.com/airbnb/enzyme/blob/master/docs/api/shallow.md)
```typescript
import React from 'react';
import {shallow} from 'enzyme';
describe(MyComponent, () => {
it('should work !', () => {
const component = shallow();
});
});
```
---
## Quelques exemples
Vérifier que le composant s'affiche avec ses sous composants
```typescript
describe(MyComponent, () => {
it('renders MyComponent and its sub components', () => {
const component = shallow(
);
expect(component.find(SubComponent1).exists()).toBeTruthy();
expect(component.find(SubComponent2).exists()).toBeTruthy();
});
});
```
Vérifier le texte du composant
```typescript
expect(component.text()).toContain('mon texte attendu');
```
Vérifier qu'une méthode a été appelée
```typescript
const mockOnChange = jest.fn();
expect(mockOnChange).toBeCalledWith(myParam);
```
---
## Quelques exemples
Simuler un événement
```typescript
component.find('button').at(1).simulate('click');
component.find('input').simulate('change', {target: {value: 'ma valeur'}});
```
Appeler une méthode
```typescript
component.instance().maMethode();
```
Récupérer l'état
```typescript
expect(component.state('name')).toEqual('mon nom');
```
---
layout: false
#TP-07
## Les tests
Le but est de tester complètement le TP-03
1. Installer jest, enzyme, react-addons-test-utils (--save-dev) et leurs définitions de type
2. Créer un fichier .test.ts par composant
3. *StudentDetails*: Vérifier le texte pour les 2 cas (Student.NULL ou non)
4. *Filter*: Vérifier que le callback est appelé lors d'un événement *change*
5. *StudenstTable*: Vérifier le nombre de ligne du tableau suivant le nombre de *Student* passés. Vérifier également le callback.
6. *StudentsApp*: Vérifier que le composant et ses sous composants sont affichés, puis vérifier l'impact des méthodes *handleSelectStudent()* et
*handleFilterChange()* sur le *state*. Enfin valider que *filteredStudents()* renvoie les bons éléments pour un filtre donné.