title | theme | revealOptions | ||
---|---|---|---|---|
React Side Effects |
solarized |
|
Slides: https://gvergnaud.github.io/react-side-effects
Gabriel Vergnaud
Héticien de la P2017
Note:
- Qui suis je ?
- gabriel vergnaud
- Heticien P2017
- developer à Sketchfab.com (On recrute!)
- gvergnaud on github
- GabrielVergnaud on twitter
mais qu'est-ce donc
Le code qui intéragit avec le monde extérieur
Mais extérieur à quoi ?
Au **scope** de la fonction
let imInTheUpperScope = true
function sideEffectFunction(params) {
// tout ce qui est défini ici
// fait partie du scope de la function `sideEffectFunction`
const imPartOfTheFunctionScope = true
// si je modifie quelque chose
// du scope parent, alors je fais un side effect:
imInTheUpperScope = false // /!\ Side effect!
// Modifier les paramètres est également un side effect
params.hello = '👋'
}
// l'ordre des définitions n'a pas d'importance
let imAlsoInTheUpperScope = true
{}
Le scope de la function est determiné par ce qui est défini à l'intérieur de ses brackets
Quelques exemples de side effects :
Modifier des variables du scope extérieur
Les requètes HTTP
L'accès à une base de donnée
Cookies, localStorage
Web APIs (DOM, WebAudio, WebGL...)
File system
Note:
- Tout ce qui touche au monde extérieur
- HTTP
- accès à une base de donnée
- Cookies, localStorage
- web APIs
- File System
// in native: side effect
const view = () => {
document.body.innerHTML = `
<div class="container">
<p>Hello world!</p>
</div>
`
}
// in React: no side effect
const View = () => {
return (
<div className="container">
<p>Hello world!</p>
</div>
)
}
React rend les intéractions avec le DOM pures et déclaratives
Note:
- Le principe de react est justement de ne pas avoir à faire de side effects pour modifier le DOM. On ne mute plus directement les DOM nodes, mais à la place on donne une configuration qui représente ce à quoi le DOM doit resembler en fonction de ses props.
Oui mais...
Le web, ce n'est pas seulement intéragir avec le DOM
quelques side effects, du dévelopement frontend
Les network requests (HTTP ou WebSocket la plupart du temps)
L'url
et l'History
pour le routing
Le localStorage
ou les cookies
pour la persistence
Les event listener
globaux (scroll, drag & drop, etc)
les autres threads
(service workers / web workers)
la console
(seulement pour le débugging a priori)
Note: Cependant, lorsque l'on fait du web, on gère d'autres types de side effects que ceux du DOM. - Des network requests (HTTP ou WebSocket la pluspart du temps) - le localStorage ou les cookies pour la persistence - L'url et l'History pour le routing - les autres threads (service workers / web workers) - parfois on a besoin d'ajouter des event listener à la main sur la window (scroll, drag & drop, etc) - la console (seulement pour le débugging a priori)
pour tout ça, on utilise useEffect
const App = () => {
useEffect(() => {
// effects
})
}
On place le code relatif aux effects dans un callback Ce callback sera runné de manière asynchrone, une fois que react a updaté le DOM.
const App = () => {
useEffect(() => {
// i can use 'user' and 'isAdmin'
}, [user, isAdmin])
}
Les dépendances sont données en deuxième paramètre À chaque fois que l'une de ses dépendances va changer, le callback sera re-exécuté Le tableau de dépendances est optionelle si il n'est pas fourni, le callback sera exécuté après chaque render
Note:
- useEffect est une API très intéressante car elle permet de penser ses side effects comme une conséquence d'un changement de data. Les dépendances sont explicite, et le code est runné à chaque fois qu'une de ses dépendance change.
const App = () => {
useEffect(() => {
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [handler])
}
À l'intérieur du useEffect, je peux retourner une fonction de cleanup Elle sera exécutée si notre composant est retiré du DOM ou si les dépendances ne sont plus à jour
Note:
- Pour certains effets, on a besoin de pouvoir les annuler lors que leur dépendances ont changé, comme le event listeners par exemple. Pour ça on peut retourner un callback de cleanup dans notre fonction d'effet.
à vos claviers
<iframe src="https://codesandbox.io/embed/wonderful-hypatia-f3g60?fontsize=14&view=editor" title="exercice use effect" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Il est possible de composer plusieurs hooks ensemble
pour créer un nouveau hook.
<iframe src="https://codesandbox.io/embed/exercice-use-effect-wz4pz?fontsize=14&view=editor" title="exercice custom hook" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Les custom hooks permettent d'encapsuler la complexité de notre code.
En résumé
useEffect est une manière de rendre déclaratif du code effectful et impératif.
Déclaratif car le code est dépendant de la data.
Note: en résumé, useEffect est manière d'abstraire du code impératif pour le rendre déclaratif, c'est à dire dépendant de la data.
Les side effects sont par nature imprevisibles.
une request HTTP peut échouer un acces au cookie peut échouer (environement sandboxé, iframes...) une manipulation de DOM peut échouer (plusieurs modifications incompatibles)
Note: Les side effects sont par nature imprevisibles.
- une request HTTP peut échouer
- un acces au cookie peut échouer (dans un environnement sandboxé comme une iframe)
- une manipulation de dom peut échouer (en cas de modification incompatible par une autre partie de notre codebase ou par l'utilisateur lui même, ses extensions etc)
Il faut donc considérer les cas d'erreur dans le code.
const p = new Promise((resolve, reject) => {
// make some effect
if (itSucceeds)
resolve(someData)
if (itFails)
reject(someError)
})
2 branches : une pour le succès et une pour l'erreur
Note: Pour ça, en javascript, on a quelque chose de pratique: les promises.
- permet de gérer le cas d'erreur
fetchUser() // returns a promise
.then(user => fetchFriends(user)) // chains a second promise
.then(friends => {}) // when all the side effects are done
on peut séquencer plusieurs effets
Note:
- permet de composer et de sequencer plusieurs side effects différents
Donc, pourquoi ne pas créer un *hook* pour utiliser des promises dans nos components ?
<iframe src="https://codesandbox.io/embed/cocky-breeze-0dxfk?fontsize=14&view=editor" title="exercice use promise" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Note:
TUTO construire un promise based useSideEffect hook
const usePromise = (getPromise, deps) => {
const [type, setType] = useState('pending')
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let isCancelled = false
setType('pending')
setData(null)
setError(null)
getPromise()
.then(data => {
if (isCancelled) return
setData(data)
setType('resolved')
})
.catch(err => {
if (isCancelled) return
setError(err)
setType('rejected')
})
return () => {
isCancelled = true
}
}, deps)
return [type, data, error]
}
On a un problème.
puisque notre reducer est une fonction pure, elle ne doit pas avoir de side effect.
Note: pottentiel gif: problème
Son type est trop restrictif
(state, action) => state
Mais pourquoi pas le changer ?
(state, action) => [state, callback]
C'est l'approche de plusieurs langages fonctionelles comme Elm ou Reason
Note: deux approches :
- Changer la signature de notre réducer:
au lieu de (state, action) => state
on passe à (state, action) => [state, effectCallback]
^ l'approche de ELM et de Reason. ça pose quand même un problème: quand est ce que l'on run les side effects ? Quand le side effect est synchrone, on peut avoir envie de l'avoir executé avant de re-render.
// CODE Sandbox ?
<iframe src="https://codesandbox.io/embed/interesting-yonath-bfou0?fontsize=14&view=editor" title="exercice use effect reducer " allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Deuxième solution :
Utiliser des middlewares
C'est l'approche de Redux
La plus courante en javascript
<iframe src="https://codesandbox.io/embed/quirky-bird-ux76p?fontsize=14&view=editor" title="exercice : effect middleware" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>
Note:
Commencer par expliquer ce qu'est un middleware
- faire un logger middleware
- simple thunk middleware
- effect middleware avec promises
Ou exécuter des side effect dans le contexte d'un state reducer ?
- utiliser un middleware
le middleware prend une fonction qui retourne une promesse et la transform en actions:
const someEffectAction = {
type: 'SOME_EFFECT_ACTION',
async effect() {
// do stuff
}
}
dispatch(someEffectAction)
Le middleware intercepte l'action et va remplacer la function effect
par de la data:
{
type: 'SOME_EFFECT_ACTION',
effect: {
type: 'pending',
error: null,
data: null
}
}
puis
{
type: 'SOME_EFFECT_ACTION',
effect: {
type: 'resolved',
error: null,
data: {...} // data returned by the promise
}
}
ou
{
type: 'SOME_EFFECT_ACTION',
effect: {
type: 'rejected',
error: Error{...}, // error thrown by the promise
data: null
}
}
C'est l'approche prise par plusieurs projets open sources, dont Hyper, le terminal de zeit.co
TUTO construire ce middleware
Quel sont les bénéfices de cette approche ?
Nos effets deviennent inspectables car ils sont représentés par des actions On peut implémenter des optimistic updates automatiques
Note:
Puisque l'on peut clairement identifier les actions qui représentent des effets, on peut créer une logique d'error handling commune.
L'optimistic update est le fait de faire comme si l'effect avait déjà eu lieu avant que la requete ai fini pour donner l'impression à l'utilisateur que notre application est super rapide.
cependant, si l'action fail, il faut rollback les changements sur le state pour bien indiquer qu'il y a eu une erreur!
Puisque l'on sait différentier une action 'pending' d'une action 'rejected', on peut automatiser ce processus pour toutes nos actions. il suffit de :
- stocker le diff du state au pending
- en cas d'erreur, réappliquer le state précédent
- en cas de succès, on peut enlever le diff que l'on avait stocké.
Les side effects sont la partie compliqué à tester. Comment simplifier le testing de notre app ?
les mocks & dependency injection
On peut encore améliorer notre middleware:
Au lieux d'importer la logique de nos side effect directement dans notre code, on peut se créer des services qui vont se charcher de faire les side effect pour nous, et les injecter via notre middleware.
Comme ça, si on est dans un contexte de testes automatisé, dans lequel on a pas envie de vraiment faire des requests, on peut injecter un mock de notre service.
// inside our code
const apiService = {
getUser(id) {
return fetch(`/users/${id}`).then(res => res.json())
}
}
effectMiddleware(apiService)
// Inside an automated test
const apiMock = {
getUser(id) {
return new Promise(resolve => {
resolve({ name: 'Gabriel', id })
})
}
}
effectMiddleware(apiMock)
de votre attention
Gabriel Vergnaud
Note:
<style> .flex { display: flex; align-items: center; justify-content: center; } .flex > *:not(:first-child) { margin-left: 10px } img.simple-image.simple-image { border:none; box-shadow:none; background: none; } .white.white.white { color: white; text-shadow: 0 2px 4px rgba(0,0,0, .5); } .lower.lower.lower { text-transform: none; } .reveal { font-size: 35px; } .reveal small { font-size: .7em; } .reveal pre { border-radius: 5px; box-shadow: 0px 8px 25px rgba(0,0,0,.25); } .reveal section img { border: none; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15) } .reveal pre code { padding: 30px; border-radius: 5px; font-weight: normal; } .reveal code { font-weight: bold; } </style>