reactReact

#Typescript

@jbcazaux

Deploys by Netlify
my name is
coffee break

Objectifs

goals

Versions

              
                  "react": "18.2.0"
                  "typescript": "4.8.4"
          
          

Le programme !

  • Typescript avancé
    • Typescript
    • Programmation fonctionnelle
  • ReactJS
    • Principes de base
    • Composants
    • Gestion de la donnée
    • Redux
    • Tests
    • Routing
    • Optimisations

Typescript

let, const


                let foo = 'bar'
                const MAX = 10
                const ttc = addTva(100, 20)
                

let vs. const ? 0-5 let / application

let, const


                const user = {}
                user.name = 'john' // (1)
                user = {name: 'lea'} // (2)
                

                const arr = ['a']
                arr.push('b') // (3)
                arr = ['a', 'b'] // (4)
                

Arrow functions


                function addOldSchool(a: number, b: number): number {
                    return a + b
                }
            

                const add = (a:number, b: number): number => {
                    return a + b
                }
                

                const add = (a:number, b: number): number => a + b
                

Arrow functions


                const squares = [0, 1, 2, 3, 4].map(x => x * x)
                console.log(squares)
                

Spread operator


              const user = {
                  firstname: 'john',
                  nickname: 'ninja',
                  lastname: 'doe',
                  age: 21
              }

              const updatedUser = {...user, firstname: 'toto'}
              console.log(updatedUser) // ?
            

Spread operator


                const odd = [1, 3, 5, 7, 9]
                const even = [2, 4, 6, 8]
                const all = [0]

                // arr.push([element1[, ...[, elementN]]])
                all.push(odd)
                all.push(even)

                console.log(all) // ?
                

Spread operator


                const odd = [1, 3, 5, 7, 9]
                const even = [2, 4, 6, 8]
                const all = [0]

                // arr.push([element1[, ...[, elementN]]])
                for (let i = 0; i < odd.length; i++) {
                    all.push(odd[i])
                }
                for (let i = 0; i < even.length; i++) {
                    all.push(even[i])
                }
                console.log(all) // ?
                

Spread operator

                
                const odd: ReadonlyArray<number> = [1, 3, 5, 7, 9]
                const even: ReadonlyArray<number> = [2, 4, 6, 8]
                const all: ReadonlyArray<number> = [0, ...odd, ...even]
                
            

Immutabilité !

Ne mutez pas pendant la formation, s'il vous plaît.

Éviter les mutations

                
                const defaultValues = [{money: 100}, {level: 0}]
                const defaultLevel = defaultValues[1]

                const newPlayer = {name: 'player1', data: defaultLevel}
                newPlayer.data.level = 2

                const newPlayer2 = {name: 'player2', data: defaultLevel}
                console.log(newPlayer2) // ?
          

Éviter les mutations

                
               const defaultValues = [{money: 100}, {level: 0}]
               const defaultLevel = defaultValues[1]

               const newPlayer = {name: 'player1', data: defaultLevel}
               const newPlayerUpdated = {...newPlayer, data: {level : 2}}
          

Destructuring


                const foo = {a: 1, b: 2, c: 'bar'}
                const {a} = foo
                console.log(a) // ?
          

Destructuring


            const user: User = {
                firstname: 'john',
                nickname: 'ninja',
                lastname: 'doe',
                age: 21
            }

            function getUserName(user: User) {
                return `${user.firstname} ${user.lastname}`
            }
            console.log(getUserName(user))
            

            function getUserName({firstname, lastname}: User) {
                return `${firstname} ${lastname}`
            }
            

            const getUserName = ({firstname, lastname}: User) =>
                `${firstname} ${lastname}`
            

Destructuring - nested


            const user: User = {
                firstname: 'john',
                lastname: 'doe',
                address: {
                    street: 'avenue des champs élysées',
                    num: '12'
                }
            }

            function getUserAddress(user: User) {
             return `${user.address.num} ${user.address.street}`
            }
            

            const getUserAddress = ({address: {num, street}}: User) =>
                `${num} ${street}`
            

Destructuring - alias


            const user: User = {
                firstname: 'john',
                lastname: 'doe',
                address: {
                    street: 'avenue des champs élysées',
                    num: '12'
                }
            }

            const getUserAddress = ({address: {num: foo, street: bar}}: User) =>
                `${foo} ${bar}`
            

Destructuring - arrays


            const maths = [x => x + 1, x => x * x, x => x * x * x]
            const [plus1, square] = maths
            plus1(41) // 42
            square(12) // 144
            

Shorthand Notation


                const foo = 42
                const bar = 1

                const longer = {
                    foo: foo,
                    bar: bar
                }
            

                const shorter = {
                    foo,
                    bar
                }
            

Promesses VS. async - await


            axios.get('/users/42/items')
                .then(resp => resp.data)
                .then(items => items.map(item => item.id))
                .then(ids => {/* use the ids */})
                .catch(error => console.log(error.toString()))
            

            const fetchItems = async () => {
                try {
                    const response = await axios.get('/users/42/items')
                    const items = response.data
                    const itemIds = items.map(item => item.id))
                    /* use the ids */
                } catch(error){
                    console.log(error.toString())
                }
            }
            

Promesses VS. async - await


            axios.get('/users/42/items')
                .then(({data : items}) => items.map(item => item.id))
                .then(ids => {/* use the ids */})
                .catch(error => console.log(error.toString()))
            

            const fetchItems = async () => {
              try {
                const {data: items} = await axios.get('/users/42/items')
                const itemIds = items.map(item => item.id))
                /* use the ids */
              } catch(error){
                console.log(error.toString())
              }
            }
            

Rappels - currying


            const add = function(x) {
                return function(y) {
                    return x + y
                }
            }
            add(2)(3) // ???
          

                const add2 = add(2)
                add2(3) // = ?
            

                const add = x => y => x + y // WTF ?!
            

                const add = x =>
                    y => (x + y)
                // easy !
            

Rappels - this


            function f() {
                this.foo // what is this ?
            }
            

react

Tout est composant

components

Hello world

Hello.tsx
            interface Props {
                name: string
            }
            const Hello = (props: Props) => 
Hello {props.name} !
export default Hello
index.tsx
            import { createRoot } from 'react-dom/client'
            import Hello from "./Hello"
            const container = document.getElementById('root')!
            const root = createRoot(container)
            root.render(<Hello name="world" />)
          

JSX


            (props: Props) => (
                 <li className="contact">
                    <h2 className="contact-name">{props.name}</h2>
                 </li>
            )
            

            (props: Props) => React.createElement('li', {className: 'contact'},
                    React.createElement('h2',
                        {className: 'contact-name'}, props.name)
                    )
            )
          

TP-01

Hello World

  1. Installer l'environnement
  2. Ecrire le composant Hello

Attributs dans les composants


            const Counter = () => {
                let count = 0

                return <div onClick={() => count = count + 1}>
                    Count: {count}
                </div>
            }
            
Don't try this at home !

State


           const ComponentWithState = (props) => {
            const [count, setCount] = useState<number>(0)
            const [user, setUser] = useState<User | null>(null)
            const [color, setColor] = useState<string>('green')

            return ...
           }
           

State

Pour mettre un état par défaut, on passe un paramètre à useState()


            const [count, setCount] = useState<number>(42)
          

Mettre à jour un état


            const [count, setCount] = useState<number>(0)
            setCount(10)
            setCount(prevState => prevState + 1)
            

Récupérer l'état courant


            const [count, setCount] = useState<number>(0)
            count
          

State


            const Counter = () =>  {
              const [count, setCount] = useState<number>(0)
              return <div onClick={() => setCount(prev => prev + 1)}>
                  Count: {count}
              </div>
            }
            

Props vs State

Props

  • Données passées par le composant parent

State

  • Données modifiées par l'utilisateur (clics, saisies, ...)
  • Données récupérées d'un web service
  • Faire un maximum de composants sans état
  • Données dérivées des props (Faire ca dans le composant directement)
  • Composants

Class components


            class ButtonApp extends React.Component {
                state = {count: 0}
                render() {
                    return <div>
                        <div>{this.props.title}</div>
                        <button>{this.props.label}</button>
                        <div>{this.state.count}</div>
                    </div>
                }
            }
            

Pourquoi les hooks ?

  • Réutiliser la logique entre les composants (sans HOC!)
  • Composants trop complexes avec les méthodes du cycle de vie (duplication, symétrie cassée sur les listeners)
  • Difficile de lire les classes : this + bind
  • Classes se "minifient" mal, le hot reloading est instable

Itérer sur un tableau


            const MyComponent = (props: Props) => (
                <ul>
                {
                    props.items.map(item =>
                        <li key={item.id}>{item.label}</li>)
                }
                </ul>
            )
            

useEffect

Déclenche un traitement lorsque des données (issues des props ou des states) changent


            const UserDetails = ({userId}: Props) => {
             const [details, setDetails] = useState<User | null>(null)

             useEffect(() => {
              axios.get<ReadonlyArray<User>>('/users/' + userId)
               .then(({data})) => setDetails(data)
             }, [userId])

             return <div>...
            }
          

TP-02

Shopping list

  1. Afficher le titre passé par le parent
  2. Mettre un état par défaut (liste vide)
  3. Mettre la liste des items retournée par l'appel au web service
  4. Afficher les items dans une liste <ul></ul>
  5. Créer un composant dédié pour chaque élément de la liste <li>
  6. Bonus: Créer un composant qui fait l'appel ajax, et passe le résultat à un composant enfant, qui lui itère sur la liste.

Bonus TP-02


            const ShoppingList = ({title}: Props) => {
               const [items, setItems] = useState<ReadonlyArray<Item>>([])
               // useEffect({...}, [])
               return <ShoppingListInternal title={title}
                        items={items}/>
            }
            const ShoppingListInternal = ({title, items}: Props) =>
            <div>
                <h2>{title}</h2>
                <ul>{items.map(item =>
                    <ShoppingItem key={item.id} item={item}/>)}
                </ul>
            </div>
            

Passer des fonctions dans les props


            const ShoppingList = ({title}: Props) => {
                const [items, setItems] = useState<ReadonlyArray<User>>([])
                // useEffect({...}, [])

                const deleteItem = (itemIdToDelete: number) => {
                 setItems(prev =>
                    prev.filter(item => item.id !== itemIdToDelete)
                 )
                }
                return (<ShoppingListInternal
                        title={title}
                        items={items}
                        del={deleteItem}/>)
                }
            }
            

Utiliser les fonctions des props


            const ShoppingListInternal = ({title, items, del}: Props) => (
                <div>
                    <h2>{title}</h2>
                    <ul>
                        {
                          items.map(item => (<li
                                    onClick={() => del(item.id)}
                                    key={item.id}
                                >
                            {item.label}: {item.price}€
                            </li>))
                        }
                    </ul>
                </div>)
            

Récupérer l'évènement JS


            const ClickableApp = () => {
                const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
                    console.log('click !', e)
                }
                return <div onClick={handleClick}>Click me !</div>
            }
            

            const ClickableApp = () => (
              <div
                onClick={(e: React.MouseEvent<HTMLDivElement>) =>
                 console.log('click !', e)}
              >
                Click me !
              </div>
            )
          

Avec du style

inline style vs css

Pourquoi ne pas inliner le css dans le js ?


            MyComponent = () => {
                const mystyle = {backgroundColor: '#F0ABCD'}
                return <div style={mystyle}/>
            }
            

Ou utiliser une css globale, une css par composant (css modules avec Webpack)


            import './MyComponent.css'
          

Ant Design

React Components for desktop applications


            <Button type="primary" onClick={handleClick}>Go!</Button>
          

Documentation

https://ant.design/docs/spec/introduce

Material-UI

React components that implement Google's Material Design


                <Button onClick={handleClick}>Go!</Button>
          

Documentation

https://material-ui.com/api/button/

Styled-Components

Visual primitives for the component age.


            const Title = styled.h1`
                font-size: 1.5em;
                text-align: center;
                color: palevioletred;
            `
            <Title>Nice title !</Title>
            

Documentation

https://www.styled-components.com/docs/api

Tips and tricks

(Workarounds)

Fragments


            const ComponentWithFragment = () => (
                <>
                    <div>Les</div>
                    <span>fragments</span>
                </>
            )
            

JSX if (else)

if


                {user && 
{user.login}
}

not


                {user || 
No User
}

if-else


                {user
                    ? 
{user.login}
:
No User
}

Pas mieux en angular :)

Commentaires


            const MyComponent = () => {
                // comment inside JS
                return <div>
                    {/* Comment inside JSX */}
                </div>
            }
            

Children


            const container = document.getElementById('root')!
            const root = createRoot(container)
            root.render(
              <ButtonApp title="My Application">Press Me! </ButtonApp>
            )
            

            export const ButtonApp = ({title, children}: Props) => {
                return <div>
                    <div>{title}</div>
                    <button>{children}</button>
                </div>
            }
            

TP-03

Liste des étudiants

  1. Créer un champ "input" qui filtre les éléments de la table
  2. Afficher le nom de l'étudiant sélectionné (Celui sur lequel on a cliqué), ou un message si aucun étudiant n'est sélectionné (par défaut)
  3. Bonus : Faire une Pull Request pour faire une jolie correction à ce TP ;)

Ce qu'on a vu jusque là

  • Un composant a des props et des états (states)
  • Un composant sans états est plus simple à écrire (et donc plus maintenable)

Flux & Redux

Flux

  • Store: Stocker la donnée
  • Action: Mettre à jour le model
  • Dispatcher: Aiguiller les actions dans les stores
  • View: Composants UI
flux

Le flux est unidirectionel ! (2-way binding)

Bad Practice vs. Good Practice

best practices
  • On ne met pas à jour les composants directement
  • On peut utiliser le store (redux), les stores (flux), les services (RxJs Subjects ), ...

Redux

Redux


                const userId = window.current_account.id
            

« Pourquoi utiliser Redux alors qu'on a déjà les variables globales ? »

#TROLL

Actions


                {type: 'INCREMENT', inc: 3}
                {type: 'ADD_TODO', text: 'Buy bread'}
                {type: 'SET_USER', user: new User(1, 'toto')}
            

Action Creator


            export const incrementor = (inc: number): IncAction => {
                return {type: 'INCREMENT', inc}
            }
            

            export const incrementor =
                (inc: number): IncAction => ({type: 'INCREMENT', inc})
            

Redux

  • Store unique (plus simple pour l'isomorphisme / SSR)
  • Le state du store est read-only
  • On dispatch des actions pour mettre à jour le state (du store)
  • Les actions sont envoyées aux reducers
  • Les reducers sont des fonctions pures
  • Les Reducers prennent un état et une action en paramètre
  • et retournent un nouvel état dans un objet 'immutable'
redux

Reducer


            export const counter = (state: number = 0, action: IncAction) => {
                switch (action.type) {
                    case 'INCREMENT':
                        return state + action.inc
                    case 'DECREMENT':
                        return state - action.inc
                    default:
                        return state
                }
            }
            
Le state est read-only ! redux

React & Redux

Composition de Reducers

Approche naïve


            const initialState: State = {user: '', counter: 0}
            export const myGlobalReducer =
                (state:State = initialState, action: Action): State => {
                    switch (action.type) {
                        case 'SET_USER':
                           return {...state, user: action.user}
                        case 'INCREMENT':
                           return {...state, counter: state.counter + 1}
                        case 'DECREMENT':
                           return {...state, counter: state.counter - 1}
                        default:
                           return state
                }
            }
            
Pas vraiment lisible pour des applications moyennes ou grosses...!

Composition de reducers


            export const user =
                (state:User|null = null, action: UserAction): User|null => {
                    switch (action.type) {
                        case 'SET_USER':
                            return action.user
                        default:
                            return state
                    }
                }
            export const counter =
                (state:number = 0, action: IncAction): number => {
                    switch (action.type) {
                        case 'INCREMENT':
                            return state + 1
                        case 'DECREMENT':
                            return state - 1
                        default:
                            return state
                    }
            }
            

Composition de reducers


            import { combineReducers } from 'redux'
            export const myGlobalReducer = combineReducers({
                user,
                counter
            })
            

Rappels

  • Actions
  • Reducers
  • Store
  • Composants React
  • Maintenant il fault lier les composants au store
redux

Les Hooks redux

  • Séparation des responsabilités: UI vs tuyauterie
  • Le code technique est fait dans les hooks

Hooks

useSelector


            import { useTypedSelector } from './reducers'

            export const MyComponent = () => {
              const user = useTypedSelector(state => state.user)
              return <div>{user.id} / {user.email} </div>
            }
            

useDispatch


            import { useDispatch } from 'react-redux'

            export const MyComponent = () => {
              const dispatch = useDispatch()
              return <div onClick={() => dispatch({type: 'INC', inc: 1})}>
                Click !
              </div>
            }
            

Connected component


          export const SelectPostalCode = () => {
           const dispatch = useDispatch()
           const currentPc = useTypedSelector(state => state.currentPc)
           const postalCodes = useTypedSelector(state => state.postalCodes)

           const handleOnChange =
              (e: React.ChangeEvent<HTMLSelectElement>) => {
            const action = setCurrentPc(e.target.value)
            dispatch(action)
           }
           return (<select onChange={handleOnChange} value={currentPc}>
           {
            postalCodes.map(pc =>
                <option key={pc} value={pc}>{pc}</option> )
           }
           </select>)
          }
          

Passer le store à l'application

Tous les composants "redux" de l'application doivent avoir accès au store.


                import { createRoot } from 'react-dom/client'
                import {Provider} from 'react-redux'
                import { legacy_createStore as createStore} from 'redux'
                import {reducer} from './reducers/index'
                import {App} from './components/app'

                const container = document.getElementById('root')!
                const root = createRoot(container)
                const store = createStore(reducer)
                root.render(<Provider store={store}>
                        <App/>
                    </Provider>)

          

TP-04

Shopping list - bis

  1. Installer redux-dev-tools
  2. Comme le TP-02, mais utiliser le state du store plutot que le state du composant, et initialiser la liste (avec un morceau de code synchrone).
  3. Creer un action creator setItems qui retourne une action
  4. Créer un reducer items qui prend une action en entrée
  5. Créer un global reducer grâce à combineReducer
  6. Créer un boutton qui ajoute un nouvel item à la liste

React Query

React Query

React Query permet de faire des requêtes et de stocker le résultat afin qu'il soit disponible dans toute l'application.


            const { data } = useQuery('items', itemsApi.get)
            

React Query

La clé fonctionne également comme un tableau de dépendances.


            const { data } =
                useQuery(['user', userId], () => usersApi.get(userId))
            

React Query

useQuery retourne bien plus que les données récupérées.


            const { data = {}, refetch, isLoading, isError } =
                useQuery(['user', userId], () => usersApi.get(userId))
            

Voir la documentation.

React Query

Pour faire des mises à jour, on utilise les mutations.


            const { mutateAsync: createUser } = useMutation(userApi.create, {
                onSuccess: () => console.log('c est OK !'),
            })
            

                <button onClick={() => createUser(myNewUser)}>CREATE USER</button>
            

Voir la documentation.

React Query

Il faut initialiser React Query à la racine de l'application


            import { createRoot } from 'react-dom/client'
            import { QueryClient, QueryClientProvider } from 'react-query'

            const container = document.getElementById('root')!
            const root = createRoot(container)
            const queryClient = new QueryClient()

            root.render(<QueryClientProvider client={queryClient}>
                <MyApp/>
              </QueryClientProvider>)
          

TP-05

Shopping list - ter

  1. Utiliser useQuery pour récupérer la liste des items (se servir de l'api existante)
  2. Utiliser useMutation pour ajouter un item à la liste
  3. Une fois l'ajout fait, utiliser refetch (renvoyé par useQuery) pour recharger la liste
  4. Utiliser isLoading pour ajouter un loader lors des temps de chargement

Bravo !

On sait créer des composants et leur passer des données

  • Depuis le composant parent
  • Depuis le LocalState
  • Depuis le store
  • Avec React Query
  • Depuis un grand(n)-parent grâce au context...

Context

Context API

  • On peut passer des données aux composants enfants (partout dans la hiérarchie).
  • Beaucoup de librairires sont basées la dessus. Ce n'était pas une API officielle avant Mars 2018.
  • React-redux fonctionne grâce à ça, le store est passé à tous les composants.

Context API

Créer un Context, et encapsuler l'application à l'interieur du <Context.Provider>


                export interface MyContext {
                    color: string
                }
                const MyContext = React.createContext<MyContext>({name: 'red'})
                export default MyContext
            

                ...
                return <MyContext.Provider
                            value={{color: 'blue'}}>
                    <SomeComponent/>
                </MyContext.Provider>
            

Context API

Ensuite on récupère le context là ou l'on veut l'utiliser avec le hook useContext()


            import {useContext} from 'react'
            import MyContext from './MyContext'

            const MyComponent = () => {
                const ctx = useContext(MyContext)
                return <div style={{backgroundColor: ctx.color}}>
                   Some text
                </div>
              }
            }
            

TP-06

Context API

L'objectif et de passer une couleur et un utilisateur d'un composant parent à un composant 'petit-fils'

  1. Encapsuer un composant React (Large) dans un Context, avec un User et une couleur.
  2. Afficher les données du context dans un composant (petit-)enfant (Small)

Tests

Jest, Enzyme, React Test Renderer, React Testing Library, ...

L'idée est de tester les composants, avec leurs états et leurs props.

L'arrivée des hooks a fait bouger la hiérarchie des outils de test.

  • Enzyme: les Hooks ont mis un moment à être supportés
  • React test renderer: Pas de façon simple de tester les évènements
  • React testing library: Des nouveaux paradigmes sur la façon de tester

Faire le rendu d'un composant


            import { render } from '@testing-library/react'

            describe('MyComponent', () => {
                it('should work !', () => {
                    render(<MyComponent/>)
                })
            })
            

React Testing Library

Tester le rendu d'un composant et ses sous-composants, avec les snapshots


            import { render } from '@testing-library/react'

            it('renders App and its sub components', () => {
                const { container } = render(<App />)

                expect(container).toMatchSnapshot()
            })
            

Assertions Jest


            it('tests', () => {
                expect(1 + 1).toEqual(2)
                expect(['a', 'b']).toHaveLength(2)
                expect(0 / 2).not.toBeNaN()
            })
            

.toEqual(value), .toBeNull(), .toBeTruthy(), .toHaveLength(number), .not.toBeNaN(), ...

Jest

Tester le rendu avec des sélecteurs


            it('should display user infos and winner', () => {
                // Given
                const { container } = render(<Podium first={player4}
                                                     second={player16}
                                                     third={player2} />)
                const playerInfos = container.querySelector('.infos')
                const winner = container.querySelector('#winner')

                // Then
                expect(playerInfos).toHaveLength(3)
                expect(winner.textContent).toMatch('player4')
            })
            

Rechercher des éléments avec les Queries RTL


          import {render, screen} from '@testing-library/react'

          it('renders Alarm', () => {
            render(<App/>)
            expect(screen.getByText('Menu Alarme')).toBeTruthy()
            expect(screen.getAllByTestId('alarme')).toHaveLength(2)
            expect(screen.getAllByRole('button')).toHaveLength(2)
          })
          

Queries

Vérifier qu'une méthode a été appelée


            const mockFunction = jest.fn()
            expect(mockFunction).toHaveBeenCalledWith(42)
          

Simuler des événements


          import {render, fireEvent, screen } from '@testing-library/react'

          it('fires events', () => {
            render(<NiceInput />)
            const input = screen.getByRole('input')

            fireEvent.change(input, {target: {value: 'new text'}})
          })
          

Async testing


          import { render, screen } from '@testing-library/react'

           it('loads data on creation', async () => {
            render(<UserInfo id={42} />)
            await screen.getByText('Jennifer')

            // expect(...)
          })
          

Mocking avancé avec Jest - functions


            import { render, screen } from '@testing-library/react'

            jest.mock('api/users', () => ({
              fetchUser: userId =>
                Promise.resolve(new User(userId, 'Jennifer')),
            }))
            describe('UserInfo Component', () => {
              it('should display user ', async () => {
                render(<UserInfo id={42} />)
                await screen.getByText('Jennifer')

                // expect...
              }
            })
            

Run tests

  • npm test in command line
  • --coverage for test coverage
  • jest runner in Webstorm / IntelliJ

TP-07

Tests

L'objectif est d'écrire les tests du TP-03

  1. Créer un fichier .test.js pour chaque composant
  2. StudentDetails: Vérifier les textes (avec Student null ou non)
  3. Filter: Verifier que le callback est appelé quand l'évènement change est propagé
  4. StudentsTable: Vérifier le nombre de ligne dans le tableau, suivant le jeu de données. Tester les appels aux callbacks aussi.
  5. StudentsApp: Vérifier que les composants sont toujours affichés de la même façon (avec un snapshot) . Tester que filteredStudents() retourne une liste filtrée.

Routing

react-router

Il y a différents Routers pour gérer l'historique de la navigation et des URLs:

  • BrowserRouter : /monsite/page1/partie2
  • HashRouter: /monsite/#/page1/partie2
  • StaticRouter: Utilisé dans le Server Side Rendering

Pour le BrowserRouter il faut rediriger toutes les requêtes vers index.html, sauf les .css, .js, et les appels aux webservices.

react-router

Il faut définir le routeur à la racine de l'application.


            import {BrowserRouter as Router,
                Routes, Route} from 'react-router-dom'
            import { createRoot } from 'react-dom/client'

            const container = document.getElementById('root')
            const root = createRoot(container)
            root.render(<Router>
                <Routes>
                  <Route path="/" element={<Home />} />
                  <Route path="/about" element={<About />} />
                  <Route path="/admin/*" element={<Admin />} >
                    <Route path="users" element={<UsersAdmin />} />
                    <Route path="articles" element={<ArticlesAdmin />} />
                  </Route>
                  <Route path="*" element={<NotFound />} />
                </Routes>
              </Router>)

            

react-router

Plusieurs façons de déclarer les routes :

  • Déclarer toutes les routes dans un seul composant, comme dans la V3. Exemple
  • Déclarer les sous routes dans chaque composant, comme dans les V4-V5. Exemple
  • Mixer les deux stratégies

react-router

Il est possible de générer les liens automatiquement (voir la documentation) doc pour l'API


            <Link to="/users">Administration des utilisateurs</Link>
            

react-router

Dynamic urls


            <Route path="users/*" element={<Users />}>
              <Route path=":userId" element={<UserDetail />} />
            </Route>
            

            const UserDetail = () => {
                const { userId } = useParams()
                return <div>
                        Manage user {userId}
                </div>
            }
            

react-router

Lorsque l'on définit les sous routes dans les composants (multiple sets of routes), il faut indiquer dans les composants où doivent apparaitrent leurs sous composants. Cela se fait avec <Outlet />


            <Route path="/admin/*" element={<Admin />} >
              <Route path="users" element={<UsersAdmin />} />
              <Route path="articles" element={<ArticlesAdmin />} />
            <Route />
            

            const Admin = () => (
              
Bienvenue sur la page d'administration <Outlet />
)

TP-08

React-router

L'objectif est de construire une application avec 3 "pages".

  1. Créer les liens vers /about et /users et les routes pour /about et /users
  2. Créer les liens vers /users/elsa et /users/anna
  3. Créer la route pour le composant UserDetail (choisir une des deux stratégies)
  4. Ecrire le lien pour les images de profil (elsa.jpeg and anna.jpeg)
  5. Créer une route * pour gérer les 404
  6. Créer une route * pour afficher un message (uniquement) lorsque aucun utilisateur n'est sélectionné
  7. Bonus : Utiliser NavLink plutôt que Link pour donner un style au lien actif
  8. Bonus : Utiliser l'autre statégie

Optimisations

Ne pas faire trop de ré-affichages inutiles

update

Reconciliation

Le Virtual DOM est... Une représentation mémoire des composants. Quand le rendu d'un composant est fait à nouveau, le nouveau V-DOM est comparé à l'ancien V-DOM. Si il y a une différence, le DOM du browser est (partiellement) mis à jour.

Trop de 're-rendering'

  • Memoization
  • useCallback()
  • useMemo()

Perfs - React.memo

React.memo permet de memoize un composant React.


           const MyComponent = () => ...
           export default React.memo(MyComponent)
          

Perfs - React.memo

React.memo permet de memoize un composant React.


           const MyComponent = () => ...
           export default React.memo(MyComponent, (prevProps, nextProps) => {
            return prevProps.user?.id === nextProps.user?.id
           })
          

Perfs - useCallback

useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.


           const MyComponent = () => (
            <Button onClick={e => console.log(e)}>Click me</>
           )
          

Perfs - useCallback

useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.


           const MyComponent = () => {
            const handleClick = (e: SyntheticEvent) => console.log(e)

            return <Button onClick={handleClick}>Click me</>
           })
          

Perfs - useCallback

useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.


           const MyComponent = () => {
            const handleClick = useCallback(e => console.log(e), [])

            return <Button onClick={handleClick}>Click me</>
           })
          

TP-09

Optimisations

  • Utiliser la memoization sur le composant Calculator pour éviter le long calcul
  • Utiliser useCallback pour éviter au composant Button de se réafficher

Let's do it !

help ? jbcazaux@gmail.com

https://formation-reactjs.fr