đź‘‹
Gabriel Vergnaud
Héticien de la P2017
Frontend engineer
Qui suis je ?
gabriel vergnaud
Heticien P2017
developer Ă Sketchfab.com (On recrute!)
gvergnaud on github
GabrielVergnaud on twitter
đź—ş
I. qu'est ce que les side effects
II. les effects en React
III. gérer les erreurs
IV. les side effects et le state reducer pattern
I
les side effects ?
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 ) {
const imPartOfTheFunctionScope = true
imInTheUpperScope = false
params.hello = 'đź‘‹'
}
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
- Qu'est ce que les side effects ?
Tout ce qui touche au monde extérieur
HTTP
accès à une base de donnée
Cookies, localStorage
web APIs
File System
const view = () => {
document .body.innerHTML = `
<div class="container">
<p>Hello world!</p>
</div>
`
}
const View = () => {
return (
<div className ="container" >
<p > Hello world!</p >
</div >
)
}
React rend les intéractions avec le DOM pures et déclaratives
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)
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(() => {
})
}
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(() => {
}, [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
dependances explicites
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
cleanup callback
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.
Il est possible de composer plusieurs hooks ensemble
pour créer un nouveau hook.
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.
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)
Il faut donc considérer les cas d'erreur dans le code.
const p = new Promise ((resolve, reject ) => {
if (itSucceeds)
resolve(someData)
if (itFails)
reject(someError)
})
2 branches : une pour le succès et une pour l'erreur
Pour ça, en javascript, on a quelque chose de pratique: les promises .
permet de gérer le cas d'erreur
fetchUser()
.then(user => fetchFriends(user))
.then(friends => {})
on peut séquencer plusieurs effets
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 ?
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]
}
Side effect et state reducer pattern
On a un problème.
puisque notre reducer est une fonction pure , elle ne doit pas avoir de side effect.
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
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 ?
Deuxième solution :
Utiliser des middlewares
C'est l'approche de Redux
La plus courante en javascript
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() {
}
}
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 : {...}
}
}
ou
{
type : 'SOME_EFFECT_ACTION' ,
effect : {
type : 'rejected' ,
error : Error {...},
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
Quels sont les bénéfices ?
optimistic updates for free
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é.
Tester les side effects
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.
const apiService = {
getUser(id) {
return fetch(<code > /users/${id}</code > ).then(res => res.json())
}
}
effectMiddleware(apiService)
const apiMock = {
getUser(id) {
return new Promise (resolve => {
resolve({ name : 'Gabriel' , id })
})
}
}
effectMiddleware(apiMock)