React Avancé
Formation de 3 jours
Le programme
Rappels
Ecmascript, Asynchrone,
React
Les hooks
1.
2.
Fonctionnement et Performance
3.
Gestion d'état et Bonus
# Intro
# Intro
Informations pratiques
- 9H - 12h30
- 14h - 17h30
Demandez-moi des pauses :)
Florent Berthelot
- 9 ans dans 2 sociétés de services (Viseo, Zenika)
- Freelance (WeFacto)
- Actuellement en mission chez Hero (paiement B2B)
- Fil rouge
- Correction à la fin de la formation
Travaux Pratique
Demandez-moi de l'aide !
Dispo sur https://berthelot.io
Slides
Je navigue de formation en formation
JavaScript
Quel est la version actuel de JavaScript ?
# JavaScript
Histoire et Rappels
Outillage
Node, NPM, Yarn, pNPM, ...
# Outillage
Outillage Node.js
# Outillage
Production Ready React App
# Outillage
Create React App
# Outillage
Create React App - TypeScript
npx create-react-app pokemon-app
# VS
npx create-react-app pokemon-app --template typescript
Exercice 1
Initiez un projet React grâce à Create React App et NPX
Observez le code et les outils qui ont été généré.
React
Rappel, JSX
Qu'est ce que React ?
# Rappel React
Pourquoi un Framework JS ?
# Rappel React
Comment React est apparu ?
Facebook !
# Rappel React
Popularité ?
# Rappel React
Les avantages
- Stable
- Stable
- JSX
- Assez performant
- Simple !
- Progressive Framework ou Lib
# Rappels
Où se positionne React ?
- Angular
- Vue.JS
- Là
Le "Hello World"
<div id="my-react-application"></div>
# Rappel React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(React.createElement('h1', {}, 'Hello world')>);
JSX
Un peu plus ?
# JSX
const domContainer = document.querySelector('#training-app-react');
ReactDOM.render(
React.createElement('main', {},
[
React.createElement('header', {},
React.createElement('h1', {}, 'Hello world')
),
React.createElement('section', {}, 'lorem ipsum')
])
, domContainer);
Encore peu plus ?
# JSX
const domContainer = document.querySelector('#training-app-react');
ReactDOM.render(
React.createElement('main', {},
[
React.createElement('header', {},
React.createElement('h1', {}, 'Hello world')
),
/** ... **/
React.createElement('section', {},
React.createElement('article', {}, [
React.createElement('p', {}, 'lorem ipsum'),
React.createElement('p', {}, 'lorem ipsum')
]),
React.createElement('article', {}, [
React.createElement('p', {}, 'lorem ipsum'),
React.createElement('p', {}, 'lorem ipsum')
])
)
/** ... **/
])
, domContainer);
JSX à la rescousse
# JSX
const domContainer = document.querySelector('#training-app-react');
ReactDOM.render(
<main>
<header>
<h1>Hello world</h1>
</header>
{/** ... **/}
<section>
<article>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</article>
<article>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</article>
</section>
{/** ... **/}
</main>
, domContainer);
Composant
Qu'est ce qu'un composant ?
<LikeButton></LikeButton>
ou
<LikeButton />
# Template
Qu'est ce qu'un composant ?
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
Qu'est ce qu'un composant ?
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
return <button>O j'aime</button>;
}
Améliorons notre code !
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime</button>;
}
Améliorons notre code !
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}</button>;
}
Et la sémantique HTML ?
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Et le style ?
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button" className="btn-primary">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Mieux ?
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className="btn-primary" style={{color: liked ? 'red' : 'grey'}}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Encore mieux ?
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Les fragments
const Tweets = () => {
const datas = [/* ... */]
return (
<React.Fragment>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</React.Fragment>
)
}
# Tips
Les fragments - version courte
const Tweets = () => {
const datas = [/* ... */]
return (
<>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</>
)
}
# Tips
Boucles
const Tweets = () => {
const items = [
{id: 1, price: 10_00},
{id: 2, price: 150_00}
]
return (
<ul>
{items.map((item) => {
return <li>{item.price}</li>;
})}
</ul>
)
}
# Tips
Boucles
const Tweets = () => {
const items = [
{id: 1, price: 10_00},
{id: 2, price: 150_00}
]
return (
<ul>
{items.map((item) => {
return <li key={item.id}>{item.price}</li>;
})}
</ul>
)
}
# Tips
Important !
Fait l'objet d'une règle ESLint
Boucles, autre exemple
const Tweets = () => {
const tweets = [
{id: 1, message: 'coucou' /* ...*/ },
{id: 2, message: 'un thread !' /* ...*/ }
]
return (
<section>
{tweets.map(tweet => {
return <Tweet key={tweet.id} tweet={tweet}/>;
})}
</section>
)
}
# Tips
Exercice 2
Créez une page sur l'application qui affiche la liste de tous les pokémons.
Interdiction d'utiliser une library autre que React 😇
Pas de hooks non plus.
Par conséquent, on utilise une liste de pokemons écrite à la main
Tests
Généralités
Tests
Et React dans tout ça ?
Un composant = une fonction sans paramètre !
# React Testing
La vue, l'enjeux des library de test de composants
# React Testing
Enzyme, Testing-Library
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import MainComponent from './main.component'
it('should display all usernames', async () => {
render(<MainComponent />)
expect(screen.getByRole('heading')).toHaveTextContent('Florent')
expect(screen.getByRole('heading')).toHaveTextContent('Agnès')
expect(screen.getByRole('heading')).toHaveTextContent('Nobel')
})
# React Testing
Un exemple
# React Testing
Comment trouver des éléments ?
# React Testing
Comment trouver des éléments ?
<div data-testid="custom-element" />
// --
import {screen} from '@testing-library/dom'
const element = screen.getByTestId('custom-element')
# React Testing
Comment trouver des éléments ?
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import MainComponent from './main.component'
it('should display all usernames', async () => {
const { container } = render(<MainComponent />);
expect(container.querySeelctor('header'))
.toHaveTextContent('Florent')
})
# React Testing
Une extension de Jest
Exercice 3
Testez la page que vous avez créé
Les props
Les fonctions ça prends des paramètres
Comment passer des données ?
# Props
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton></LikeButton>
Comment passer des données ?
# Props
Composant
Vue
// like.component.jsx
const LikeButton = (props) => {
const nbOfLike = props.nbOfLike;
const liked = props.liked;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked={true} nbOfLike={1} />
Propriétés
Mieux ?
# Props
Composant
Vue
// like.component.jsx
const LikeButton = (props) => {
const nbOfLike = props.nbOfLike;
const liked = props.liked;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked nbOfLike={1} />
Propriétés
Destructuring ?!
# Props
Composant
Vue
// like.component.jsx
const LikeButton = ({
nbOfLike,
liked
}) => {
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked nbOfLike={1} />
Propriétés
Exercice 4
Faire un composant qui affiche le nom et le poids du Pokémon.
L'utiliser dans notre composant principal.
Il faut toujours tester :)
Events
&
Hooks
Le state
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton />
Propriétés
Le state
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const NbLikeState = React.useState(0);
return (
<button type="button">
{NbLikeState[0]} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
Le state : Destructuring !
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# State
Instant bonne pratique.
Tout ce qui est dans un React.useState doit être totalement indépendant des autres useState.
Qu'est ce qu'un effet de bords ?
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
UseEffect
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setEllapsedTime((ellapsedTime) => ellapsedTime + 1);
}, 1000)
}, [])
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
Fuite mémoire ?
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setEllapsedTime((ellapsedTime) => ellapsedTime + 1);
}, 1000)
return () => {
clearInterval(interval);
}
}, [])
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
Les évènements
Comment écouter le click ?
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
onClick ! (camelCase)
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button" onClick={() => setNbOfLike(nbOfLike++)}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
Ses propres évènements ?
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
const Tweet = () => {
const [nbOfLikes, setNbOfLikes] = useState(0);
return <LikeButton nbOfLikes={nbOfLikes} onLike={() => setNbOfLikes(nbOfLikes++)} />
}
Comment tester des évènements ?
# Évenement
L'empathie !
Comment tester des évènements ?
# Évenement
Comment tester des évènements ?
# Évenement
import { fireEvent, render, screen } from '@testing-library/react';
it('should display 1 like when cliking on the button', () => {
render(<LikeButton />);
fireEvent(
screen.getByText('0 like'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
)
screen.getByText('1 like')
})
Mieux ?
# Évenement
# Évenement
import { fireEvent, render, screen } from '@testing-library/react';
it('should display 1 like when cliking on the button', () => {
render(<LikeButton />);
fireEvent.click(screen.getByText('0 like'))
screen.getByText('1 like')
})
Encore mieux ?
# Évenement
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
it('should display 1 like when cliking on the button', () => {
render(<LikeButton />);
await userEvent.click(screen.getByText('0 like'));
screen.getByText('1 like');
})
Exercice 5
Utilisez une API pour récupérer les Pokémons
Quand on clique sur un Pokémon, alors il est affiché comme sélectionné.
On utilise Fetch
React, fin des rappels
Valeurs par défaut
const LikeButton = ({
onLike = () => {},
nbOfLike = 0
}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Rappels Fin
Valeurs par défaut
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
LikeButton.defaultProps = {
onLike: () => {},
nbOfLike: 0
}
# Rappels Fin
Validation de props
import PropTypes from 'prop-types';
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
LikeButton.defaultProps = {
nbOfLike: 0
}
LikeButton.proptypes = {
onLike: PropTypes.func.isRequired,
nbOfLike: PropTypes.number
}
# Rappels Fin
Affichage conditionnel
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{
isUserTweet ?
<EditButton /> :
null
}
</article>
)
}
# Rappels Fin
Affichage conditionnel ?
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet && <EditButton />}
</article>
)
}
# Tips
Affichage conditionnel ?
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet ?? <EditButton />}
</article>
)
}
# Tips
Transclusion, children, ...
const Parent = () => {
return (
<Modal>
<h2>lorem ipsum</h2>
<button>close</button>
</Modal>
}
const Modal = ({children}} => {
return (
<div className="modal">
{children}
</div>
)
}
# Tips
Exercice 6
Les Pokémons qui ont un poids inférieur à 60 et seulement eux sont mit en avant avec un texte pour les inciter à manger plus
Astuce : Utilisez les balises HTML detail / summary
Les références
# Refs
Qu'est ce que c'est ?
Un moyen d’accéder aux nœuds du DOM ou éléments React
# Refs
Quand ?
- Besoin d'accéder au DOM pour gérer le positionnement
- Interfaçage avec une API tierce
- Lancer des animations, ...
# Refs
Quand ?
- Besoin d'accéder au DOM pour gérer le positionnement
- Interfaçage avec une API tierce
- Lancer des animations, ...
Cas d'usage rare, a utiliser rarement donc.
Le cas jamais utilisé
class AnExample extends React.Component {
constructor(props) {
super(props);
this.refToChildren = React.createRef();
}
componentDidMount() {
this.refToChildren.current.sayHello();
}
render() {
return (
<ISayHello ref={this.refToChildren} />
);
}
# Refs
Le cas jamais utilisé
class ISayHello extends React.Component {
constructor(props) {
super(props);
this.sayHello = this.sayHello.bind(this);
}
sayHello() {
console.log('hello la formation')
}
render() {
return (
<></>
);
}
}
# Refs
Le cas jamais utilisé
class AnExample extends React.Component {
constructor(props) {
super(props);
this.refToChildren = React.createRef();
}
componentDidMount() {
this.refToChildren.current.sayHello();
}
render() {
return (
<ISayHello ref={this.refToChildren} />
);
}
# Refs
Ne marche pas avec les Functional Component
Le cas utile
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
focusTextInput() {
this.textInput.current.focus();
}
componentDidMount() {
this.focusTextInput()
}
render() {
return (
<>
<input
type="text"
ref={this.textInput} />
</>
);
}
}
# Refs
Le cas utile
function CustomTextInput(props) {
const textInput = useRef(null);
useEffect(() => {
textInput.current.focus();
}, [])
return (
<>
<input
type="text"
ref={textInput} />
</>
);
}
# Refs
Le transfert de Refs
const Main = () => {
const theRef = useRef(null);
return <Coucou ref={theRef} />
}
const Coucou = () => {
return (
<main>
<section>
{/* ... */}
</section>
<section>
{/* ... */}
</section>
</main>
);
}
# Refs
Où va la ref ?
Le transfert de Refs
const Main = () => {
const theRef = useRef(null);
return <Coucou ref={theRef} />
}
const Coucou = React.forwardRef((props, ref) => {
return (
<main ref={ref}>
<section>
{/* ... */}
</section>
<section>
{/* ... */}
</section>
</main>
);
})
# Refs
Où va la ref ?
Et React 19 ?
# Refs
Où va la ref ?
Exercice 7
Lorsque l'on passe la souris sur un pokémon, une infobulle se déploie avec les stats du pokémon.
Cette infobulle - qui est un composant - est positionné grâce à un élément Ancre.
Exercice 7
Exemple d'API
// Composant Parent
const APP = () => {
/** ... **/
return (
<>
<PokemonCard ref={ref} />
<Infobulle anchor={ref} isOpen={true}>
Some text here
</Infobulle>
</>
)
}
// Composant Infobulle
const Infobulle = ({anchor}) => {
const anchorRect = anchor.current?.getBoundingClientRect();
const rightOfAnchor = anchorRect ? anchorRect.x + anchorRect.width : 0
const bottomOfAnchor = anchorRect ? anchorRect.bottom + 8 : 0
const x = rightOfAnchor
const y = bottomOfAnchor
/** ... **/
}
Les Hooks !
Mais avant...
Comment ça fonctionne React ?
# Les hooks
Règle #1 :
Toujours appeler les hooks depuis un composant React.
# Les hooks
Règle #2 :
Toujours appeler les hooks au plus haut niveau de la fonction de rendu.
(Pas dans une boucle, pas dans un if, etc.)
# Les hooks
Quel est l'interêt ?
- Réutiliser de la logique !
- Donner du sens au lifecycle
UseMemo
const Einstein = () => {
const result = React.useMemo(() => whatIsTheLifeGoal(), []);
return result;
}
# Les hooks
UseMemo
const Einstein = ({
humanName
}) => {
const result = React.useMemo(
() => whatIsTheLifeGoal(humanName),
[humanName]
);
return result;
}
# Les hooks
UseMemo
const Einstein = ({
humanName
}) => {
const result = React.useMemo(() => whatIsTheLifeGoal(humanName));
return result;
}
# Les hooks
UseEffect
const Tweet = ({id}) => {
useEffect(() => {
console.log('A new render occured');
})
return result;
}
# Les hooks
UseEffect
const Tweet = ({id}) => {
useEffect(() => {
console.log('Id changed', id);
}, [id])
return result;
}
# Les hooks
UseEffect
const Tweet = ({id}) => {
useEffect(() => {
console.log('Initial id', id);
}, [])
return result;
}
# Les hooks
UseCallback
const TweetEdit = ({id}) => {
const handleSubmit = useCallback(() => {
fetch(`addTweet?userId=${id}`)
}, [id])
return <TweetForm onSubmit={handleSubmit} />;
}
# Les hooks
Un hook custom ?
const TweetEdit = ({id}) => {
const [inputValue, setInputValue] = useState(initialValue);
const onInput = (e) => {
setInputValue(e.target.value);
}
return <input type="text" onInput={onInput} value={inputValue} />;
}
# Les hooks
Un hook custom !
const useInput = (initialValue) => {
const [inputValue, setInputValue] = useState(initialValue);
const onInput = (e) => {
setInputValue(e.target.value);
}
return [inputValue, onInput];
}
const TweetEdit = ({id}) => {
const [value, onInput] = useInput()
return <input type="text" onInput={onInput} value={value} />;
}
# Les hooks
UseReducer
const initialState = {hpPokemon1: 500, hpPokemon2: 500};
function reducer(state, action) {
switch (action.type) {
case 'pokemon1Attack':
return {...state, hpPokemon2: state.hpPokemon2 - 50};
case 'pokemon2Attack':
return {...state, hpPokemon1: state.hpPokemon1 - 50};
default:
throw new Error();
}
}
const PokemonArena = ({id}) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch('pokemon1Attack');
}, [])
return <PokemonBattle hpPokemon1={state.hpPokemon1} hpPokemon2={state.hpPokemon2}>;
}
# Les hooks
UseState Vs useReducer ?
# Les hooks
Quand il y a plusieurs variables liée qui se mettent à jour en même temps, je préfère le useReducer.
C'est moins error-prone de faire des dispatchs dans ces cas là.
D'autre hooks ?
# Les hooks
Bonus : Comment les Hooks sont codé
# Les hooks
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
# Les hooks
Qu'est ce que React.StrictMode
useEffect(()=>{
console.log('How many time I render ?');
}, []);
# Les hooks
StrictMode
useEffect(()=>{
console.log('How many time I render ?');
}, []);
# Les hooks
StrictMode
2 fois ou plus !
Le but ? Être certain que vous faite un bon cleanup et que certaines choses n'arriverons pas en production.
Exercice 8
Lorsque 2 Pokémons sont sélectionnés, il faut maintenant qu'ils s'affrontent jusqu'à ce qu'il y ai un vainqueur.
Utilisez les hooks.
Allez au plus simple sur les règles de combats.
D'ailleurs, pour votre gestion de requête. useReducer ou useState ?
Devtools
Installation
# Devtools
Demo Time On :
https://react-devtools-tutorial.vercel.app/
Display Name
const MyComponent = () => {
return <h1>Hello World</h1>;
}
# Devtools
Display Name
const MyComponent = React.forwardRef((props, ref) => {
return <h1 ref={ref}>Hello World</h1>;
})
// Comment trouver le nom de ce composant ??
# Devtools
Display Name
const MyComponent = React.forwardRef((props, ref) => {
return <h1 ref={ref}>Hello World</h1>;
});
MyComponent.displayName = "MyComponent";
// Aussi utile si vous minifier le code ;)
# Devtools
UseDebugValue
const Tweet = ({id}) => {
// Affiche l'ID dans les Devtools React
useDebugValue(id);
return result;
}
# Devtools
Exercice 9
Installez les devtools et visualiser l'arbre de vos composant
Gestion des Erreurs
Gestion des erreurs
const Tweet = () => {
return (
<ErrorBoundary>
<LikeButton />
</ErrorBoundary>
)
}
const LikeButton = () => {
throw new Error('Not Implemented');
}
# Error boundary
Gestion des erreurs
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
sendErrorToSentry(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Page d'erreur.</h1>;
}
return this.props.children;
}
}
# Error boundary
# Error Boundary
Attention
Cela ne gère pas les erreurs dans les évènements ! C'est uniquement pour les erreurs critiques.
Exercice 10
Faites en sorte d'afficher "oops" quand une erreur arrive dans votre application.
Performance
# Performance
Avant-propos
La recherche de la performance arrive en dernier dans le développement.
Vraiment en dernier.
Genre uniquement quand il y a des problèmes de perfs. Pas avant.
React Lazy
# Les perfs
Attention : Ceci n'est disponible côté server que depuis la version 18.
React Lazy
# Les perfs
React Lazy
# Les perfs
import React, { Suspense } from 'react';
const MyBeautifulComponentThatUseD3JS = React.lazy(
() => import('./MyBeautifulComponentThatUseD3JS')
);
const MainComponent = () => {
return (
<div>
<header>
<h1>Mon super graph</h1>
</header>
<Suspense fallback={<div>Chargement...</div>}>
<MyBeautifulComponentThatUseD3JS />
</Suspense>
</div>
);
}
React Lazy - Devtools
# Les perfs
Un problème de perf ?
# Les perfs
Encore les devstools !
Quelques pistes d'améliorations
# Les perfs
1 - Corriger la cause avant d'optimiser !
2 - React.memo
3 - useCallback
4 - useMemo
React.memo
const MyInput = React.memo(function TheNameIsNotUsefullHer(props) {
console.log('myInput rendered');
/* ... */
});
const Main = () => {
const [toto, setToto] = useState(0);
return (
<>
<MyInput toto={toto} />
<button onClick={() => setToto(42)}>Push It</button>
</>
)
}
# Les perfs
React.memo
const MyInput = React.memo(function TheNameIsNotUsefullHer(props) {
console.log('myInput rendered');
/* ... */
});
const Main = () => {
const [toto, setToto] = useState({anObject: 42});
return (
<>
<MyInput toto={toto} />
<button onClick={() => setToto({anObject: 42})>Push It</button>
</>
)
}
# Les perfs
React.memo
const MyInput = React.memo(function TheNameIsNotUsefullHer(props) {
console.log('myInput rendered');
/* ... */
});
const Main = () => {
const [toto, setToto] = useState(0);
const handleInputValueChange = () => {
/** ... **/
}
return (
<>
<MyInput toto={toto} onChange={handleInputValueChange} />
<button onClick={() => setToto(42)}>Push It</button>
</>
)
}
# Les perfs
UseCallback
const MyInput = React.memo(function TheNameIsNotUsefullHer(props) {
console.log('myInput rendered');
/* ... */
});
const Main = () => {
const [toto, setToto] = useState(0);
const handleInputValueChange = useCallback(() => {
/** ... **/
}, [])
return (
<>
<MyInput toto={toto} onChange={handleInputValueChange} />
<button onClick={() => setToto(42)}>Push It</button>
</>
)
}
# Les perfs
UseMemo
const Einstein = ({
humanName
}) => {
const result = React.useMemo(() => whatIsTheLifeGoal(humanName));
return result;
}
# Les perfs
Exercice 11
Ajouter un Timer afficher la durée de la bataille entre Pokémon.
Le stockage des données de ce Timer se trouve au plus haut de l'application.
Étudiez et optimisez le nombre de rendu de l'application
React Query
AKA TanStack Query
# HTTP
React = Library
Donc pas de manière officiel pour récupérer des données :/
Fetch
const Tweet = () => {
const [fetchState, setFetchState] = useState({
error: null,
data: null,
isLoading: true
});
useEffect(() => {
fetch('/people.json')
.then(res => res.json())
.then(data => {
setFetchState({
data,
isLoading: false
})
});
})
.catch(err => {
setFetchState({
error: err,
isLoading: false
})
});
}, [])
if(fetchState.isLoading) {
return 'loading...':
}
if(fetchState.error) {
return <>Refresh page</>
}
return <>{fetchState.data}</>
}
# HTTP
React Query !
# HTTP
React Query !
npm i @tanstack/react-query
# HTTP
React Query !
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Tweet />
</QueryClientProvider>
)
}
const Tweet = () => {
const { isLoading, error, data } = useQuery('peoples', () =>
fetch('/peoples.json').then(res => res.json())
);
if (isLoading) {
return 'Loading...';
}
if (error) {
return 'Please refresh the page.';
}
return <>{data}</>
}
# HTTP
Et les tests ?
# React Query
it('should display the tweet', async () => {
nock('https://api.twitter.com')
.get('/tweet/1')
.reply(200, {
tweet: {
id: 1,
message: 'this is what happens when you don’t recycle your pizza boxes'
},
})
const queryClient = new QueryClient();
render(
<QueryClientProvider client={queryClient}>
<Tweet id="1" />
</QueryClientProvider>
)
await waitFor(() => {
expect(screen.getByText(/loading.../i)).not.toBeInDocument();
})
expect(
screen
.getByText(/this is what happens when you don’t recycle your pizza boxes/i)
).toBeInTheDocument();
})
# React Query
Un exemple de test
Marre des wrapper ?
# React Query
React 19
# React Query
Exercice 12
Utilisez React-Query pour récupérer les données
A l'aide, j'ai trop de props 😱
C
C
C
C
C
C
C
C
C
State
State
State
State
State
State
State
State
State
// app.tsx
const App = () => {
return (
<MyApp giveIt={'toMe'} />
)
}
// MyApp.tsx
const MyApp = (props) => {
return (
<OtherCmp {...props} />
)
}
// OtherCmp.tsx
const OtherCmp = ({giveIt}) => {
return (
<div>{giveIt} gnon</div>
)
}
# Context
// Fichier A
const CookieContext = React.createContext(null);
export const CookieProvider = CookieContext.Provider;
export const useCookie = () => React.useContext(CookieContext);
// // app.tsx
const App = () => {
return (
<CookieProvider value={{giveIt: "toMe"}}>
<MyApp />
</CookieProvider>
)
}
// MyApp.tsx
const MyApp = () => {
return (
<OtherCmp />
)
}
// OtherCmp.tsx
const OtherCmp = () => {
const {giveIt} = useCookie()
return (
<div>{giveIt} gnon</div>
)
}
# Context
# Context
React 19
Exercice 13
Le state du matchmaking doit maintenant être dans un contexte
Fin
Florent Berthelot
https://berthelot.io
florent@berthelot.io
... avant les bonus
Correction des TPs
Les Bonus
La gestion d'état à l'échelle
MobX, Redux & co
Redux VS MVC
MobX
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"
// Model the application state.
class Timer {
secondsPassed = 0
constructor() {
makeAutoObservable(this)
}
increase() {
this.secondsPassed += 1
}
reset() {
this.secondsPassed = 0
}
}
const myTimer = new Timer()
// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
<button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
// Update the 'Seconds passed: X' text every second.
setInterval(() => {
myTimer.increase()
}, 1000)
# State managment
Les patterns
High Order Function
const twice = f => x => f(f(x));
const plusThree = i => i + 3;
// Composition de fonction !
const plusSix = twice(plusThree);
console.log(plusSix(7)); // 13
# Patterns
High Order Component
const LogIt = (WrappedComponent) => {
const Component = (props) => {
console.log('redered', props);
return <WrappedComponent {...props} />;
}
return Component;
}
# Patterns
High Order Component
# Patterns
High Order Component
# Patterns
Render Props
# Patterns
<Controller
control={control}
name="test"
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, isTouched, isDirty, error },
formState,
}) => (
<Checkbox
onBlur={onBlur} // notify when input is touched
onChange={onChange} // send value to hook form
checked={value}
inputRef={ref}
/>
)}
/>
Render Props - Attention
# Patterns
- Cas d'usage assez Rare
- Préférer l'utilisation de Children (nesting)
- Si c'est pour partager de la logique => Hooks
Les portals
<html>
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html>
# Portal
Admettons ce HTML
const appRoot = document.getElementById('app-root');
const App = () => {
return (
<Modal>
<h2>La formation ?</h2>
<button role="button">React Avancé !</button>
</Modal>
);
}
render(<App />, appRoot);
# Portal
App
const modalHost = document.getElementById('modal-root');
const Modal = ({children}) => {
return createPortal(
children,
modalHost,
);
}
# Portal
La modal
<html>
<body>
<div id="app-root"></div>
<div id="modal-root">
<h2>La formation ?</h2>
<button role="button">React Avancé !</button>
</div>
</body>
</html>
# Portal
Le HTML final
TypeScript
# TypeScript
Avant propos
Create React App
# TypeScript
Trichez !
# TypeScript
Composants Polymorphique
# Polymorph
Pourquoi ?
Parfois, on essai de faire des composants réutilisable.
Ces composants étant central, respectons le FIRST Principle !
- Focus
- Independent
- Reusable
- Small
- Testable
FIRST Principle
Je vais faire mes propres boutons
const MyButton = ({onClick, children}) => {
return <button className="monSuperStyle" onClick={onClick}>{children}</button>
}
// dans un composant
<MyButton onClick={goToHomePage}>
Elle est top mon API de mon boutton !
</MyButton>
# Polymorph
Et les bonnes pratiques ?!
const MyButton = ({onClick, ...otherProps}) => {
return (
<button {...otherProps} className={`monSuperStyle ${otherProps.className}`}>
{children}
</button>
);
}
// dans un composant
<MyButton onClick={goToHomePage}>
Elle est top mon API de mon boutton !
</MyButton>
# Polymorph
Mais en faite c'est un lien !
const MyButton = ({onClick, ...otherProps}) => {
return (
<button {...otherProps} className={`monSuperStyle ${otherProps.className}`}>
{children}
</button>
);
}
// dans un composant
<MyButton href="/" as="a">
Elle est top mon API de mon boutton !
</MyButton>
# Polymorph
Là c'est bien !
const MyButton = ({onClick, as, ...otherProps}) => {
return React.createElement(
as,
{
...otherProps,
className: `monSuperStyle ${otherProps.className}`,
},
children
);
}
// dans un composant
<MyButton href="/" as="a">
Elle est top mon API de mon boutton !
</MyButton>
# Polymorph
Le Style !important
Simple !
import 'app.css';
# Style
Mais c'est global ??
Css modules à la rescousse...
import styles from './Button.module.css';
const Button = () => {
return <button className={styles.error}>Error Button</button>;
}
# Style
Css modules à la rescousse...
import styles from './Button.module.css';
const Button = () => {
return <button className={styles.error}>Error Button</button>;
}
# Style
<button class="Button_error_ax7yz">Error Button</button>
Styled-component
Style-component
const Button = styled.a`
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
margin: 0.5rem 1rem;
width: 11rem;
background: transparent;
color: white;
border: 2px solid white;
${props => props.primary && css`
background: white;
color: black;
`}
`
# Style
Style-component
const App = () => {
return (
<form>
<Button type="submit" as="input" value="Envoyer" />
</form>
);
}
# Style
Les tests E2E
Cypress, Playwright
Installation
$ npm install cypress --save-dev
# Cypress
{
"scripts": {
"cypress:open": "cypress open"
}
}
describe('Pokemon', () => {
it('should start battle between pickachu and salamèche', () => {
cy.visit('https://localhost:3000/')
cy.contains('Pickachu').click()
cy.contains('Salamèche').click()
cy.get('main')
.should('contain', 'Pickachu contre Salamèche, le combat commence !')
})
})
# Cypress
Exemple de test
# Cypress
Des limitations
Les tests tournent dans une iFrame.
Difficile de faire des tests impliquant plusieurs sites web, ...
Playwrite a l'air de ne pas avoir ces limites. A voir...
React Router
# router
Qu'est ce qu'une SPA ?
# Router
Pourquoi faire une SPA ?
# Router
Quel sont les limites d'une SPA ?
# Router
Avant propos
Client
Page web
Serveur
html + css + js + Data
# Router
Avant propos
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (Json)
React Router - Init
import { BrowserRouter } from "react-router-dom";
// ...
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// ...
# Router
React Router - Point d'injection
import { Routes, Route, Link } from "react-router-dom";
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit" element={<EditTweet />} />
</Routes>
</div>
);
}
# Router
React Router - Les liens ?
import { Routes, Route, Link } from "react-router-dom";
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/">Mes tout 8</Link>
<Link to="/edit">Tout 8, et ?</Link>
</nav>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit" element={<EditTweet />} />
</Routes>
</div>
);
}
# Router
React Router - Les params ?
import { Routes, Route, Link } from "react-router-dom";
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/">Mes tout 8</Link>
<Link to="/edit/toto">Tout 8, et ?</Link>
</nav>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit/:tweetId" element={<EditTweet />} />
</Routes>
</div>
);
}
const EditTweet = () => {
const { tweetId } = useParams();
return <>id: {tweetId}</>
}
# Router
React Router - Les search params ?
import { Routes, Route, Link } from "react-router-dom";
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/">Mes tout 8</Link>
<Link to="/edit?id=toto">Tout 8, et ?</Link>
</nav>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit" element={<EditTweet />} />
</Routes>
</div>
);
}
const EditTweet = () => {
let [searchParams, setSearchParams] = useSearchParams();
return <>id: {searchParams.get('id')}</>
}
# Router
React Router - Redirection ?
import { Routes, Route, Link } from "react-router-dom";
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/">Mes tout 8</Link>
<Link to="/edit">Tout 8, et ?</Link>
</nav>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit" element={<EditTweet />} />
</Routes>
</div>
);
}
const EditTweet = () => {
return <Navigate to="/" />
}
# Router
React Router - Redirection JS
function App() {
return (
<div className="App">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/">Mes tout 8</Link>
<Link to="/edit">Tout 8, et ?</Link>
</nav>
<Routes>
<Route path="/" element={<Tweets />} />
<Route path="/edit" element={<EditTweet />} />
</Routes>
</div>
);
}
const EditTweet = () => {
let navigate = useNavigate();
useEffect(() => {
navigate("/");
}, []);
return 'hello';
}
# Router
React Router - Lazy Loading
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Chargement...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
# Router
Les Formulaires
# Form
Composant Controlé ou non ?
# Form
Composants Contrôlé =
Le parent est la source de vérité. Tout changement passe par le parent.
Un hook custom !
const useInput = (initialValue) => {
const [inputValue, setInputValue] = useState(initialValue);
const onChange = (e) => {
setInputValue(e.target.value);
}
return [inputValue, onChange];
}
const TweetEdit = ({id}) => {
const [value, onChange] = useInput()
return <input type="text" onChange={onChange} value={value} />;
}
# Les hooks
Exemple complet
const TweetEdit = ({id}) => {
const [valueInput, onChangeInput] = useInput('')
const [valueSelect, onChangeSelect] = useInput('false')
const [valueTextArea, onChangeTextArea] = useInput('')
const handleSubmit = (e) => {
e.preventDefault();
// ...
}
return (
<form onSubmit={handleSubmit}>
<label>
Titre :
<input type="text" name="title" onChange={onChangeInput} value={valueInput} />
</label>
<label>
Message :
<textarea name="message" value={valueTextArea} onChange={onChangeTextArea} />
</label>
<label>
NSFW :
<select value={valueSelect} onChange={onChangeSelect}>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</label>
<button type="submit">Envoyer</submit>
</form>
);
}
# Les hooks
# Form
Composant Non Controlé
L'enfant a son propre state, on va chercher l'information généralement via les Refs
Composant non-controlé
const useInput = (initialValue) => {
const [inputValue, setInputValue] = useState(initialValue);
const onChange = (e) => {
setInputValue(e.target.value);
}
return [inputValue, onChange];
}
const TweetEdit = ({id}) => {
const [value, onChange] = useInput()
return <input type="text" onChange={onChange} value={value} />;
}
# PRESENTING CODE
# Forms
React = Library
Donc c'est très manuel, ou alors faut une autre library.
Un lib ?
React-Hook-Form !
Ne pas utiliser Formik (pas stable)
React-Hook-Form
import { useState } from "react";
import { useForm } from "react-hook-form";
import Header from "./Header";
export function App() {
const { register, handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
// ...
console.log(data)
})
return (
<form onSubmit={onSubmit}>
<input {...register("firstName")} placeholder="Titre" />
<select {...register("NSFW")}>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
<textarea {...register("message")} placeholder="Un T-8 !" />
<button type="submit" >Envoyer</button>
</form>
);
}
# Form
React-Hook-Form
# Form
- Validation
- Intégration avec Zod https://react-hook-form.com/docs/useform#resolver
- Refaire Composant controllé - non-controllé
Et React 19 ?
# Form
Next.js
SPA classique
# Next.js
Next.JS
React
1 fichier / APP
SPA classique (CSR)
# Next.js
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (JSON)
Server Side Rendering (SSR)
# Next.js
Client
APP Angular
Serveur
html + css + js
API
Java, Node, ...
more data (JSON)
+ initial data
data (JSON)
Next.js
Static Side Generation (SSG)
# Next.js
Next.JS
React
1 fichier HTML / page
API
Static Site Generation (SSG)
# Next.js
Client
APP React
Serveur
html + css + js
+ data
Statique
API
Java, Node, ...
more data (JSON)
Incremental Static Regeneration (ISR)
# Next.js
Client
APP React
Serveur
html + css + js
API
Java, Node, ...
more data (JSON)
+ initial data
data (JSON)
Next.js
Serveur
Static
# Next.js
Concrètement ?
Le seul gros changement, c'est le router !
Il se fait fichier par fichier !
I18n
React Avancé
By Florent Berthelot
React Avancé
- 219