Formation de 3 jours
Rappels
Ecmascript, Asynchrone,
React
Les hooks
Fonctionnement et Performance
Gestion d'état et Bonus
# Intro
# Intro
- 9H - 12h30
- 14h - 17h30
Demandez-moi des pauses :)
- 9 ans dans 2 sociétés de services (Viseo, Zenika)
- Freelance (WeFacto)
- Actuellement en mission chez Hero (paiement B2B)
Demandez-moi de l'aide !
Dispo sur https://berthelot.io
Je navigue de formation en formation
Quel est la version actuel de JavaScript ?
# JavaScript
Node, NPM, Yarn, pNPM, ...
# Outillage
# Outillage
# Outillage
# Outillage
npx create-react-app pokemon-app
# VS
npx create-react-app pokemon-app --template typescript
Initiez un projet React grâce à Create React App et NPX
Observez le code et les outils qui ont été généré.
Rappel, JSX
# Rappel React
# Rappel React
Facebook !
# Rappel React
# Rappel React
- Stable
- Stable
- JSX
- Assez performant
- Simple !
- Progressive Framework ou Lib
# Rappels
- Angular
- Vue.JS
- Là
<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
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);
# 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
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);
<LikeButton></LikeButton>
ou
<LikeButton />
# Template
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
return <button>O j'aime</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# 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>;
);
}
# 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>;
);
}
# 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>;
);
}
const Tweets = () => {
const datas = [/* ... */]
return (
<React.Fragment>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</React.Fragment>
)
}
# Tips
const Tweets = () => {
const datas = [/* ... */]
return (
<>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</>
)
}
# Tips
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
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
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
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
Généralités
Et React dans tout ça ?
# React Testing
# 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
# React Testing
# React Testing
<div data-testid="custom-element" />
// --
import {screen} from '@testing-library/dom'
const element = screen.getByTestId('custom-element')
# React Testing
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
Testez la page que vous avez créé
Les fonctions ça prends des paramètres
# 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>
# 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
# 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
# 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
Faire un composant qui affiche le nom et le poids du Pokémon.
L'utiliser dans notre composant principal.
Il faut toujours tester :)
# 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
# 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
# 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
Tout ce qui est dans un React.useState doit être totalement indépendant des autres useState.
# 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
# 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
# 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
# É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
# É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
# É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++)} />
}
# Évenement
L'empathie !
# Évenement
# É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')
})
# É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')
})
# É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');
})
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
const LikeButton = ({
onLike = () => {},
nbOfLike = 0
}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Rappels Fin
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
LikeButton.defaultProps = {
onLike: () => {},
nbOfLike: 0
}
# Rappels Fin
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
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{
isUserTweet ?
<EditButton /> :
null
}
</article>
)
}
# Rappels Fin
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet && <EditButton />}
</article>
)
}
# Tips
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet ?? <EditButton />}
</article>
)
}
# Tips
const Parent = () => {
return (
<Modal>
<h2>lorem ipsum</h2>
<button>close</button>
</Modal>
}
const Modal = ({children}} => {
return (
<div className="modal">
{children}
</div>
)
}
# Tips
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
# Refs
Un moyen d’accéder aux nœuds du DOM ou éléments React
# Refs
- Besoin d'accéder au DOM pour gérer le positionnement
- Interfaçage avec une API tierce
- Lancer des animations, ...
# Refs
- 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.
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
class ISayHello extends React.Component {
constructor(props) {
super(props);
this.sayHello = this.sayHello.bind(this);
}
sayHello() {
console.log('hello la formation')
}
render() {
return (
<></>
);
}
}
# Refs
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
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
function CustomTextInput(props) {
const textInput = useRef(null);
useEffect(() => {
textInput.current.focus();
}, [])
return (
<>
<input
type="text"
ref={textInput} />
</>
);
}
# 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 ?
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 ?
# Refs
Où va la ref ?
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.
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
/** ... **/
}
Comment ça fonctionne React ?
# Les hooks
Toujours appeler les hooks depuis un composant React.
# Les hooks
Toujours appeler les hooks au plus haut niveau de la fonction de rendu.
(Pas dans une boucle, pas dans un if, etc.)
# Les hooks
- Réutiliser de la logique !
- Donner du sens au lifecycle
const Einstein = () => {
const result = React.useMemo(() => whatIsTheLifeGoal(), []);
return result;
}
# Les hooks
const Einstein = ({
humanName
}) => {
const result = React.useMemo(
() => whatIsTheLifeGoal(humanName),
[humanName]
);
return result;
}
# Les hooks
const Einstein = ({
humanName
}) => {
const result = React.useMemo(() => whatIsTheLifeGoal(humanName));
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('A new render occured');
})
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('Id changed', id);
}, [id])
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('Initial id', id);
}, [])
return result;
}
# Les hooks
const TweetEdit = ({id}) => {
const handleSubmit = useCallback(() => {
fetch(`addTweet?userId=${id}`)
}, [id])
return <TweetForm onSubmit={handleSubmit} />;
}
# Les hooks
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
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
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
# 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à.
# Les hooks
# Les hooks
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
# Les hooks
useEffect(()=>{
console.log('How many time I render ?');
}, []);
# Les hooks
useEffect(()=>{
console.log('How many time I render ?');
}, []);
# Les hooks
2 fois ou plus !
Le but ? Être certain que vous faite un bon cleanup et que certaines choses n'arriverons pas en production.
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
Demo Time On :
https://react-devtools-tutorial.vercel.app/
const MyComponent = () => {
return <h1>Hello World</h1>;
}
# Devtools
const MyComponent = React.forwardRef((props, ref) => {
return <h1 ref={ref}>Hello World</h1>;
})
// Comment trouver le nom de ce composant ??
# Devtools
const MyComponent = React.forwardRef((props, ref) => {
return <h1 ref={ref}>Hello World</h1>;
});
MyComponent.displayName = "MyComponent";
// Aussi utile si vous minifier le code ;)
# Devtools
const Tweet = ({id}) => {
// Affiche l'ID dans les Devtools React
useDebugValue(id);
return result;
}
# Devtools
Installez les devtools et visualiser l'arbre de vos composant
const Tweet = () => {
return (
<ErrorBoundary>
<LikeButton />
</ErrorBoundary>
)
}
const LikeButton = () => {
throw new Error('Not Implemented');
}
# Error boundary
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
Cela ne gère pas les erreurs dans les évènements ! C'est uniquement pour les erreurs critiques.
Faites en sorte d'afficher "oops" quand une erreur arrive dans votre application.
# Performance
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.
# Les perfs
Attention : Ceci n'est disponible côté server que depuis la version 18.
# Les perfs
# 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>
);
}
# Les perfs
# Les perfs
Encore les devstools !
# Les perfs
1 - Corriger la cause avant d'optimiser !
2 - React.memo
3 - useCallback
4 - useMemo
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
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
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
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
const Einstein = ({
humanName
}) => {
const result = React.useMemo(() => whatIsTheLifeGoal(humanName));
return result;
}
# Les perfs
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
AKA TanStack Query
# HTTP
Donc pas de manière officiel pour récupérer des données :/
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
# HTTP
npm i @tanstack/react-query
# HTTP
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
# 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
# React Query
# React Query
Utilisez React-Query pour récupérer les données
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
Le state du matchmaking doit maintenant être dans un contexte
Florent Berthelot
https://berthelot.io
florent@berthelot.io
... avant les bonus
MobX, Redux & co
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
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
const LogIt = (WrappedComponent) => {
const Component = (props) => {
console.log('redered', props);
return <WrappedComponent {...props} />;
}
return Component;
}
# Patterns
# Patterns
# Patterns
# 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}
/>
)}
/>
# Patterns
- Cas d'usage assez Rare
- Préférer l'utilisation de Children (nesting)
- Si c'est pour partager de la logique => Hooks
<html>
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html>
# Portal
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
const modalHost = document.getElementById('modal-root');
const Modal = ({children}) => {
return createPortal(
children,
modalHost,
);
}
# Portal
<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
# TypeScript
# TypeScript
# TypeScript
# Polymorph
Parfois, on essai de faire des composants réutilisable.
Ces composants étant central, respectons le FIRST Principle !
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
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
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
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
import 'app.css';
# Style
Mais c'est global ??
import styles from './Button.module.css';
const Button = () => {
return <button className={styles.error}>Error Button</button>;
}
# Style
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>
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
const App = () => {
return (
<form>
<Button type="submit" as="input" value="Envoyer" />
</form>
);
}
# Style
Cypress, Playwright
$ 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
# Cypress
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...
# router
# Router
# Router
# Router
Client
Page web
Serveur
html + css + js + Data
# Router
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (Json)
import { BrowserRouter } from "react-router-dom";
// ...
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
// ...
# Router
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
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
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
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
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
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
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
# Form
# Form
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
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
L'enfant a son propre state, on va chercher l'information généralement via les Refs
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
Donc c'est très manuel, ou alors faut une autre library.
React-Hook-Form !
Ne pas utiliser Formik (pas stable)
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
# Form
- Validation
- Intégration avec Zod https://react-hook-form.com/docs/useform#resolver
- Refaire Composant controllé - non-controllé
# Form
# Next.js
Next.JS
React
1 fichier / APP
# Next.js
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (JSON)
# Next.js
Client
APP Angular
Serveur
html + css + js
API
Java, Node, ...
more data (JSON)
+ initial data
data (JSON)
Next.js
# Next.js
Next.JS
React
1 fichier HTML / page
API
# Next.js
Client
APP React
Serveur
html + css + js
+ data
Statique
API
Java, Node, ...
more data (JSON)
# 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
Le seul gros changement, c'est le router !
Il se fait fichier par fichier !