Logomotion: React + Redux
This commit is contained in:
parent
7c0f21ae7f
commit
74a2e26f18
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
|
@ -0,0 +1,1137 @@
|
|||
<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
|
||||
|
||||
Une représentation proche du XML d'une arborescence de composants. Il ne faut pas faire d'amalgame, **ce n'est pas du HTML**.
|
||||
|
||||
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`
|
||||
|
||||
Elle permettent de passer des données aux composants.
|
||||
|
||||
```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() });
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## Évènements du DOM
|
||||
|
||||
Les évènements du DOM sont interceptés par le passage de "callbacks" sur les propriétés `on<Event>` des composants.
|
||||
|
||||
```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 }))
|
||||
}
|
||||
// La méthode decrement() incrémente la valeur du compteur de 1
|
||||
decrement() {
|
||||
this.setState(prevState => ({ count: prevState.count-1 }))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gestion des styles
|
||||
|
||||
Différentes méthodologies existes pour gérer les styles des composants.
|
||||
|
||||
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
|
||||
|
||||
<StyledComponent 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Si l'action n'est pas gérée, on retoure l'état
|
||||
// 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', () => {
|
||||
// On créait une instance de notre store
|
||||
// avec le state par défaut
|
||||
const store = configureStore()
|
||||
|
||||
// On créait un "faux" subscriber
|
||||
// pour vérifier que l'état du store
|
||||
// à bien été modifié le nombre de fois voulu
|
||||
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() {
|
||||
// On créait une liste à partir des éléments présents
|
||||
// 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/)
|
Loading…
Reference in New Issue