2018-03-12 21:20:13 +01:00
< style >
pre { font-size: 0.5em !important; }
table { font-size: 0.6em !important; }
< / style >
# React + Redux
## William Petit - S.C.O.P. Cadoles
---
## Amorçage d'un projet
---
<!-- page_number: true -->
## Méthode simplifiée: `create-react-app`
```bash
npm install -g create-react-app # Installation du générateur d'application
create-react-app my-app # Génération de l'application
cd my-app # Se placer dans le répertoire
npm start # Lancer le serveur de développement
```
https://github.com/facebook/create-react-app
---
## Méthode manuelle
### Installation des dépendances
```bash
mkdir my-app # Création du répertoire de l'application
cd my-app # Se placer dans le répertoire
npm init # Initialiser le projet NPM
# Installation des dépendances NPM
npm install --save \
react react-dom react-hot-loader \
webpack webpack-dev-server \
webpack-cli \
babel-core babel-loader \
babel-preset-env babel-preset-react \
babel-preset-stage-2 \
style-loader file-loader css-loader \
html-webpack-plugin clean-webpack-plugin \
uglifyjs-webpack-plugin webpack-merge
```
---
### Configuration de Webpack
```js
// my-app/webpack.common.js
const webpack = require('webpack')
const HTMLPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [
{ test: /\.(js|jsx)$/, exclude: /node_modules/, use: ['babel-loader'] },
{ test: /\.(scss|sass)$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/, use: ['file-loader'] }
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
plugins: [
new webpack.ProvidePlugin({
'React': 'react'
}),
new HTMLPlugin({
title: 'MyApp',
hash: true,
template: 'src/index.html'
})
]
}
```
---
```js
// my-app/webpack.dev.js
const webpack = require('webpack')
const common = require('./webpack.common.js')
const merge = require('webpack-merge')
const path = require('path')
module.exports = merge(common, {
devtool: 'inline-source-map',
mode: 'development',
plugins: [
new webpack.HotModuleReplacementPlugin()
],
output: {
path: path.join(__dirname, 'public'),
publicPath: '/',
filename: '[name].[hash].js'
},
devServer: {
contentBase: './public',
hot: true
}
})
```
---
```
// my-app/webpack.prod.js
const common = require('./webpack.common.js')
const merge = require('webpack-merge')
const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = merge(common, {
mode: 'production',
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin(['dist']),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new UglifyJSPlugin({
sourceMap: true
})
]
})
```
---
### Compléter le fichier `package.json`
```js
// my-app/package.json
{
"scripts": {
"start": "webpack-dev-server --progress --colors --config webpack.dev.js",
"build": "webpack --progress --colors --config webpack.prod.js"
},
// ...
"babel": {
"presets": [
"env",
"react",
"stage-2"
],
"plugins": ["react-hot-loader/babel"]
}
}
```
---
### Création des fichiers source de base
```html
<!-- my - app/src/index.html -->
<!DOCTYPE html>
< html >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > MyApp< / title >
< % for (var css in htmlWebpackPlugin.files.css) { %>
< link href = "<%= htmlWebpackPlugin.files.css[css] %>" rel = "stylesheet" >
< % } %>
< body >
< div id = "app" > < / div >
< / body >
< / html >
```
https://github.com/jantimon/html-webpack-plugin
---
```js
// my-app/src/index.js
import ReactDOM from 'react-dom'
import App from './app'
ReactDOM.render(< App / > , document.getElementById('app'))
```
```js
// my-app/src/app.js
import { Component } from 'react'
import { hot } from 'react-hot-loader'
class App extends Component {
render () {
return < h1 > Hello World !< / h1 >
}
}
export default hot(module)(App)
```
---
### Lancer le serveur de développement
```bash
npm start # Puis ouvrir http://localhost:8080 dans votre navigateur
```
### Générer une version pour la production
```bash
npm run build # Les fichiers générés seront dans le répertoire my-app/dist
```
---
## Les composants
---
## Différentes notations
### Classe ES6
```jsx
// my-app/src/components/hello-world.js
import React from 'react'
class HelloWorld extends React.Component {
render() {
return (
< div className = 'hello-world' >
< h1 > Hello World< / h1 >
< p > Welcome to React< / p >
< / div >
)
}
}
export default HelloWorld
```
---
## Notation fonctionnelle
```jsx
// my-app/src/components/hello-world.js
const HelloWorld = () => {
return (
< div className = 'hello-world' >
< h1 > Hello World< / h1 >
< p > Welcome to React< / p >
< / div >
)
}
export default HelloWorld
```
---
## La syntaxe JSX
2018-03-22 18:08:34 +01:00
C'est une représentation proche du XML d'une arborescence de composants. Il ne faut pas faire d'amalgame, **ce n'est pas du HTML** .
2018-03-12 21:20:13 +01:00
La définition des attributs propres au DOM se fait via le nom de la propriété Javascript, pas de l'attribut HTML. **Par exemple l'attribut`class` en HTML devient `className` en JSX** .
Il en est de même pour la **définition des styles** .
---
## `props`
2018-03-22 18:08:34 +01:00
Elles permettent de passer des données aux composants.
2018-03-12 21:20:13 +01:00
```jsx
// my-app/src/components/props-example.js
import React from 'react'
class PropsExample extends React.Component {
render() {
return (
< span > { this.props.text }< / span >
)
}
}
export default PropsExample
// Exemple d'utilisation
// < PropsExample text = "foo bar" / >
```
---
### `props.children`
La propriété `children` permet de récupérer les composants 'enfants' qui pourraient être passés au composant lors de son rendu.
```jsx
// my-app/src/components/my-list.js
import React from 'react'
class MyList extends React.Component {
render() {
return (
< ul >
{ this.props.children }
< / ul >
)
}
}
export default PropsExample
// Exemple d'utilisation
< MyList >
< li > Item 1< / li >
< li > Item 2< / li >
< / MyList >
```
---
## `state`
La propriété `state` d'un composant représente l'état (au sens données) de celui ci.
```jsx
// my-app/src/components/clock.js
import React from 'react'
export default class Clock extends React.Component {
constructor(props) {
super(props)
// Initialisation du "state" du composant
this.state = {
time: new Date()
}
// On appelle la méthode tick() du composant
// toutes les secondes
setInterval(this.tick.bind(this), 1000);
}
// Méthode de rendu du composant
render() {
return (
< div > Time: { this.state.time.toString() }< / div >
)
}
// La méthode tick() met à jour le state du composant avec
// la date courante
tick() {
this.setState({ time: new Date() });
}
}
```
---
2018-03-22 18:08:34 +01:00
## Événements du DOM
2018-03-12 21:20:13 +01:00
2018-03-22 18:08:34 +01:00
Les événements du DOM sont interceptés par le passage de "callbacks" sur les propriétés `on<Event>` des composants.
2018-03-12 21:20:13 +01:00
```jsx
// my-app/src/components/counter.js
import React from 'react'
export default class Counter extends React.Component {
constructor(props) {
super(props)
// Initialisation du "state" du composant
this.state = {
count: 0
}
// On "lie" les méthodes de la classe à l'instance
this.increment = this.increment.bind(this)
this.decrement = this.decrement.bind(this)
}
// Méthode de rendu du composant
render() {
return (
< div >
Count: < span > { this.state.count }< / span >
< button onClick = { this . increment } > +1< / button >
< button onClick = { this . decrement } > -1< / button >
< / div >
)
}
// La méthode increment() incrémente la valeur du compteur de 1
increment() {
this.setState(prevState => ({ count: prevState.count+1 }))
}
2018-03-22 18:08:34 +01:00
// La méthode decrement() décrémente la valeur du compteur de 1
2018-03-12 21:20:13 +01:00
decrement() {
this.setState(prevState => ({ count: prevState.count-1 }))
}
}
```
---
## Gestion des styles
2018-03-22 18:08:34 +01:00
Différentes méthodologies existent pour gérer les styles des composants.
2018-03-12 21:20:13 +01:00
La méthode classique CSS (avec ou sans préprocesseur SCSS/SASS) est tout à fait viable.
Les styles des composants sont également souvent déclarés en Javascript et utilisés via les `props` des composants.
D'autres solutions plus avancées comme [styled-components ](https://www.styled-components.com/ ) ou [CSS Modules ](https://github.com/css-modules/css-modules ) existent également. Elles mettent l'accent sur la modularité et la réutilisation des règles de style entre les composants.
---
### Déclaration des styles en Javascript
```jsx
// my-app/src/components/my-styled-component.styles.js
export default {
self: {
backgroundColor: 'red',
border: '1px solid black',
padding: '10px'
},
content: {
backgroundColor: 'blue',
border: '1px dotted white'
}
}
```
```jsx
// my-app/src/components/my-styled-component.js
import React from 'react'
import styles from './my-styled-component.styles.js'
export default class MyStyledComponent extends React.Component {
render() {
return (
< div style = {styles.self} >
< div style = {styles.content} >
Isnt it pretty ?
< / div >
< / div >
)
}
}
```
---
## Composition des styles en Javascript
En utilisant l'opérateur de décomposition `...` , il est possible
de composer les styles.
```jsx
// my-app/src/components/my-styled-component.js
import React from 'react'
import styles from './my-styled-component.styles.js'
export default class MyStyledComponent extends React.Component {
render() {
return (
< div style = {{...styles.self, . . . this . props . style } } >
< div style = {{...styles.content, . . . this . props . contentStyle } } >
Isnt it pretty ?
< / div >
< / div >
)
}
}
// Exemple d'utilisation
< Style dComponent style = {{backgroundColor: ' green ' } } contentStyle = {{border: ' none ' } } / >
```
---
## Cycle de vie et méthodes
https://reactjs.org/docs/react-component.html
---
### Séquence de montage
1. `constructor(props)`
2. `componentWillMount()`
3. `render()`
4. `componentDidMount()`
---
### Séquence de mise à jour
1. `componentWillReceiveProps(nextProps)`
2. `shouldComponentUpdate(nextProps, nextState)`
3. `componentWillUpdate(nextProps, nextState)`
4. `render()`
5. `componentDidUpdate(prevProps, prevState)`
---
### Séquence de démontage
1. `componentWillUnmount()`
---
### Gestion des erreurs
`componentDidCatch(error, info)`
Permet d'intercepter les erreurs qui pourraient être émises dans l'arbre des sous composants.
---
## Composants "contrôlés" (formulaires)
```jsx
// my-app/src/components/my-form.js
import React from 'react'
export default class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(evt) {
this.setState({value: evt.target.value});
}
handleSubmit(evt) {
console.log(`Votre nom est ${this.state.name}`);
evt.preventDefault();
}
render() {
return (
< form onSubmit = {this.handleSubmit} >
< label >
Nom:
< input type = "text" value = {this.state.name} onChange = {this.handleChange} / >
< / label >
< input type = "submit" value = "Soumettre" / >
< / form >
);
}
}
```
---
## Intégrer React avec d'autres librairies
https://reactjs.org/docs/integrating-with-other-libraries.html
---
## Tester ses composants
https://facebook.github.io/jest/docs/en/tutorial-react.html
### Installer Jest
```bash
npm install --save-dev jest react-test-renderer babel-jest
```
### Configurer le script NPM
```js
// package.json
{
// ...
"scripts" : {
"test": "jest"
}
//...
}
```
---
## Créer un script de test
```
// my-app/src/components/my-styled-component.test.js
/* globals test, expect */
import MyStyledComponent from './my-styled-component'
import renderer from 'react-test-renderer'
test('Navbar snapshot', () => {
const component = renderer.create(< MyStyledComponent / > )
let tree = component.toJSON()
// Vérifier que le composant n'a pas changé depuis le dernier
// snapshot.
// Voir https://facebook.github.io/jest/docs/en/snapshot-testing.html
// pour plus d'informations
expect(tree).toMatchSnapshot()
// L'API expect() de Jest est disponible à l'adresse
// https://facebook.github.io/jest/docs/en/expect.html
// Il est possible d'effectuer des vérifications plus avancées
// grâce au projet Enzyme (vérification du DOM, etc)
// Voir http://airbnb.io/enzyme/ et
// https://facebook.github.io/jest/docs/en/tutorial-react.html#dom-testing
})
```
---
### Lancer les tests
```bash
npm test
```
---
## Gestion des routes
https://reacttraining.com/react-router/web/
### Installation du module
```bash
npm install --save react-router react-router-dom
```
---
### Déclaration basique des routes
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route, Switch, Redirect } from 'react-router'
import { Clock, MyStyledComponent, My404Page } from './components'
export default class App extends React.Component {
render() {
< HashRouter >
< Switch >
< Route path = '/clock' exact component = {Clock} / >
< Route path = '/styled' exact component = {MyStyledComponent} / >
< Redirect from = '/old-style' to = '/styled' / >
< Route component = {My404Page} / >
< / Switch >
< / HashRouter >
}
}
```
---
## Liens de navigation
```
// my-app/src/components/my-linked-component.js
import React from 'react'
import { Link } from 'react-router'
export default class App extends React.Component {
render() {
return (
< div >
< Link to = '/styled' / >
< / div >
)
}
}
```
---
## Paramètres de route
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route } from 'react-router'
import { MyRouteParameter } from './components'
export default class App extends React.Component {
render() {
< HashRouter >
< Route path = '/:myParam' component = {MyRouteParameter} / >
< / HashRouter >
}
}
```
```
// my-app/src/components/my-route-parameter.js
import React from 'react'
import { HashRouter } from 'react-router-dom' // ou BrowserRouter
import { Route } from 'react-router'
import { MyRouteParameter } from './components'
export default class App extends React.Component {
render() {
const { match } = this.props
< span > Paramètre: { match.params.myParam }< / span >
}
}
```
---
### Sous routes
```
// my-app/src/app.js
import React from 'react'
import { HashRouter } from 'react-router-dom'
import { Route, Switch } from 'react-router'
import { ProductPage } from './components'
export default class App extends React.Component {
render() {
< HashRouter >
< Switch >
< Route path = '/products/:productId' component = {ProductPage} / >
< / Switch >
< / HashRouter >
}
}
```
```
// my-app/src/component/product.js
import React from 'react'
import { Route, Switch } from 'react-router'
import ProductView from './product-view'
import ProductEdit from './product-edit'
export default class ProductPage extends React.Component {
render() {
const { match } = this.props
return (
< Switch >
< Route path = {`${match.url}/view`} component = {ProductView} / >
< Route path = {`${match.url}/edit`} component = {ProductEdit} / >
< / Switch >
)
}
}
```
---
## Et plus encore...
- [Transitions ](https://reacttraining.com/react-router/web/example/animated-transitions )
- [Liens personnalisés ](https://reacttraining.com/react-router/web/example/custom-link )
- [Chemins récursifs... ](https://reacttraining.com/react-router/web/example/recursive-paths )
---
## État de l'application
---
## Architecture Flux
![center 100% ](img/flux-simple-f8-diagram-with-client-action-1300w.png )
---
## Redux
https://redux.js.org/
---
## Flot des données
![center 100% ](./img/redux_flow.png?1 )
---
## `store`, `actions` et `reducers`
### Générateurs d'actions
```js
// actions.js
export const ADD_PRODUCT = 'ADD_PRODUCT'
export function addProduct (name, price) {
return {type: ADD_PRODUCT, product: {name, price}}
}
export const REMOVE_PRODUCT = 'REMOVE_PRODUCT'
export function removeProduct (name) {
return {type: REMOVE_PRODUCT, productName: name}
}
```
---
### "Reducer" racine
```js
// reducer.js
export default const rootReducer = (state, action) => {
console.log(`Action: ${JSON.stringify(action)}`)
switch (action.type) {
case ADD_PRODUCT:
// L'action est de type ADD_PRODUCT
// On ajoute le produit dans la liste et
// on retourne un nouvel état modifié
return {
products: [...state.products, action.product]
}
case REMOVE_PRODUCT:
// L'action est de type REMOVE_PRODUCT
// On filtre la liste des produits et on
// retourne un nouvel état modifié
return {
products: state.products.filter(p => p.name !== action.productName)
}
}
2018-03-22 18:08:34 +01:00
// Si l'action n'est pas gérée, on retourne l'état
2018-03-12 21:20:13 +01:00
// sans le modifier
return state
}
```
---
## Création du `store`
```
// store.js
import { createStore } from 'redux'
import rootReducer from './reducer'
export function configureStore(initialState = { products: [] }) {
return createStore(
rootReducer,
initialState
)
}
```
---
## Tester
```
// store.test.js
/* globals test, expect, jest */
import { addProduct, removeProduct } from './actions'
import { configureStore } from './store'
test('Ajout/suppression des produits', () => {
2018-03-22 18:08:34 +01:00
// On crée une instance de notre store
2018-03-12 21:20:13 +01:00
// avec le state par défaut
const store = configureStore()
2018-03-22 18:08:34 +01:00
// On crée un "faux" subscriber
2018-03-12 21:20:13 +01:00
// pour vérifier que l'état du store
2018-03-22 18:08:34 +01:00
// a bien été modifié le nombre de fois voulu
2018-03-12 21:20:13 +01:00
const subscriber = jest.fn()
// On attache notre faux subscriber
// au store
store.subscribe(subscriber)
// On "dispatch" nos actions
store.dispatch(addProduct('pomme', 5))
store.dispatch(addProduct('orange', 7))
store.dispatch(removeProduct('pomme'))
// On s'assure que notre subscriber a bien été
// appelé
expect(subscriber).toHaveBeenCalledTimes(3)
const state = store.getState()
// On s'assure que l'état du store correspond
// à ce qu'on attend
expect(state).toMatchObject({
products: [
{name: 'orange', price: 7}
]
})
})
```
---
## Actions asynchrones
https://github.com/gaearon/redux-thunk
```bash
npm install --save redux-thunk
```
---
### Ajout du middleware au `store`
```
// store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducer'
export function configureStore(initialState = { products: [] }) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk) // On ajoute le middleware 'thunk'
)
}
```
---
## Générateur d'actions asynchrones
```
// actions.js
export const SAVE_PRODUCT = 'SAVE_PRODUCT'
export const SAVE_PRODUCT_SUCCESS = 'SAVE_PRODUCT_SUCCESS'
export const SAVE_PRODUCT_FAILURE = 'SAVE_PRODUCT_FAILURE'
export function saveProduct (product) {
return (dispatch, getState) => {
dispatch({type: SAVE_PRODUCT, product})
return fetch('http://my-api/products', {
method: 'POST',
body: JSON.stringify(product)
})
.then(res => res.json())
.then(res => dispatch({ type: SAVE_PRODUCT_SUCCESS, product: res.data }))
.catch(error => dispatch({ type: SAVE_PRODUCT_FAILURE, error }))
}
}
```
---
## Utilisation avec React
### Installer les dépendances
```bash
npm install --save react-redux
```
---
### Déclaration de l'usage du `store` dans l'application
```
// my-app/src/index.js
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './app'
import { configureStore } from './store'
const store = configureStore()
ReactDOM.render(
< Provider store = {store} >
< App / >
< / Provider > ,
document.getElementById('app')
)
```
---
```
// my-app/src/app.js
import React from 'react'
import { addProduct } from './actions'
import { connect } from 'react-redux'
class App extends React.Component {
componentDidMount() {
// Toutes les 2 secondes, on "dispatch" une nouvelle
// action qui va ajouter un produit dans le store
setInterval(() => {
this.props.dispatch(addProduct(`My product #${Date.now()}`, Math.random()*10|0))
}, 2000)
}
render() {
2018-03-22 18:08:34 +01:00
// On crée une liste à partir des éléments présents
2018-03-12 21:20:13 +01:00
// dans le tableaux de produits
const items = this.props.products.map((p, i) => {
return < li key = {i} > {p.name}< / li >
})
return (
< ul >
{items}
< / ul >
)
}
}
// Cette fonction permet de "sélectionner" les éléments
// qui nous intéressent dans le state et de les exposer
// en tant que "props" sur notre composant
const mapStateToProps = state => {
return {
products: state.products
}
}
// On "emballe" notre composant dans un "higher order component" (ou H.O.C.)
// ce qui nous permet de récupérer les données à partir du store
export default connect(mapStateToProps)(App)
```
---
## Mise en application
---
### Contexte
Implémenter une application de type "catalogue" comprenant les fonctionnalités suivantes:
- Créer/modifier/supprimer des fiches "produit"
- Trouver des produits via un système de recherche multicritères
- Un mécanisme d'authentification par mot de passe
- Un système de rôles basiques:
- Un rôle "administrateur" pouvant effectuer toutes les actions
- Un rôle "éditeur" pouvant créer/modifier/supprimer des fiches existantes
- Un rôle "consultant" permettant de lire/rechercher des fiches existantes
---
### Contraintes
Vous pouvez sélectionner n'importe quel type de produit à implémenter (album de musique, collection de pins, bestiaire de jeu de rôle...). Voici cependant les différents types d'attributs qui devront être présents sur une fiche produit:
- Un attribut de type numérique
- Un attribut de type "liste de mots clés"
- Un attribut de type "texte libre"
- Un attribut de type "fichier"
La partie "backend" est laissée à votre libre choix.
---
# Licence
## CC BY-NC-SA 3.0 FR
[Creative Commons - Attribution - Pas d’ Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 France ](https://creativecommons.org/licenses/by-nc-sa/3.0/fr/ )