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

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

Made with Slides.com