Functional components et Hooks :
La nouvelle méthode React

Présenté par Franck ALARY

React, c'est quoi ?

Une bibliothèque JavaScript pour créer des interfaces utilisateurs

Moue... mais encore ?

import React from 'react'
import ReactDOM from 'react-dom';

const HelloName = props => <div className="hello">Hello {props.name} !</div>

ReactDOM.render(
  <HelloName name="Franck" />,
  document.getElementById('app')
);
  • Une approche basée sur la création de composants.
  • En vulgarisant : Créer vos propres "balises HTML" avec un comportement qui leur est propre.

Voici un functional component.

Et ?

Plusieurs fonctions pour faciliter les interactions utilisateur et le rafraichissement des données sur votre page.

import React, { useState } from 'react'
import ReactDOM from 'react-dom';

const Increment = () => {
  const [counter, setCounter] = useState(0)
  
  return (
    <div className="counter" onClick={() => setCounter(counter + 1)}>
      {counter}
    </div>
  )
}

ReactDOM.render(
  <Increment />,
  document.getElementById('app')
);

Voici un Hook

C'est dur à installer ?

Vous devez avoir préalablement installé Nodejs.

npx create-react-app mon-app
cd mon-app
npm start

Et le tour est joué !

Les Composants

Qu'un seul élément
direct retourné

export default props => (
  <div>
    <h1>{props.title}</h1>
    <p>{props.message}</p>
  </div>
)
export default props => (
  <>
    <h1>{props.title}</h1>
    <p>{props.message}</p>
  </>
)
export default props => (
  <React.Fragment>
    <h1>{props.title}</h1>
    <p>{props.message}</p>
  </React.Fragment>
)

React.Fragment

Un composant peut
retourner null

export default props => props.text === undefined ? null : <p>{props.text}</p>
export default props => props.text !== undefined && <p>{props.text}</p>

props.children

Récupérer les enfants du composant

export default props => (
  <div className="container">
    {props.children}
  </div>
)
export default props => {
  const [title, text] = ({
    1:['Mon titre 1', 'Mon texte 1'],
    2:['Mon titre 2', 'Mon texte 2'],
    3:['Mon titre 3', 'Mon texte 3'],
  })[props.idContent]
  return props.children(title, text)
}
export default props => (
  <GetContent idContent={3}>{
    (title, text) => (
      <>
        <h1>{title}</h1>
        <p>{text}</p>
      </>
    )
  }</GetContent>
)

GetContent.js

DisplayContent.js

export default props => (
  <Container>
    <p>Mon texte</p>
  </Container>
)

Les styles

import React from 'react'

import './MyStyle.css' // CSS Global
import styles1 from './MyStyle.module.css' // CSS pour le composant
import styles2 from './MyStyle.module.scss' // Sass pour le composant

export default props => (
  <div className={myStyles1.container}>
    <h1 className={[styles1.title, styles2.title].join(' ')}>Titre</h1>
    {props.children}
  </div>
)

CSS module implémenté et configuré par défaut avec npx create-react-app.

npm install node-sass

Pour ajouter Sass au projet

Les Hooks

Liste non exhaustive
(Les très utiles)

useState

import React, { useState } from 'react'
import ReactDOM from 'react-dom';

const Increment = () => {
  const [counter, setCounter] = useState(0)
  
  return (
    <div className="counter">
      <p>{counter}</p>
      <ul>
        <li onClick={() => setCounter(counter - 1)}>-</li>
        <li onClick={() => setCounter(counter + 1)}>+</li>
      </ul>
    </div>
  )
}

ReactDOM.render(
  <Increment />,
  document.getElementById('app')
)

useEffect

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom';

const Increment = () => {
  const [counter, setCounter] = useState(0)
  
  useEffect(() => {
    alert('Le composant vient d\'être initialisé dans le navigateur !')
  }, [])
  useEffect(() => {
    alert('Le composant vient d\'être actualisé dans le navigateur !')
  })
  useEffect(() => {
    alert('Le state counter vient d\'être mis à jour dans le navigateur !')
  },[counter])
  
  return (
    <div className="counter" onClick={() => setCounter(counter + 1)}>
      {counter}
    </div>
  )
}

ReactDOM.render(
  <Increment />,
  document.getElementById('app')
)

useEffect

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const CountDown = props => {
  const [counter, setCounter] = useState(10)

  useEffect(
    () => {counter > 0 && setTimeout(() => setCounter(counter - 1), 1000)},
    [counter]
  )

  return <div>{counter}</div>
}

ReactDOM.render(
  <CountDown/>,
  document.getElementById('app')
)

useEffect

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom';

const MyMessage = props => {
  useEffect(
    () => alert('Message vient d\'être mise à jour dans le navigateur !'), 
    [props.message]
  )
  
  return <div>{props.message}</div>
}

ReactDOM.render(
  <MyMessage message="Hello World !" />,
  document.getElementById('app')
)

useEffect

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom';

const FadeIn = props => {
  const [classComponent, setClassComponent] = useState('hidden-block')
  
  useEffect(() => setClassComponent('displayed-block'), [])
  
  return <div className={classComponent}>{props.message}</div>
}

ReactDOM.render(
  <FadeIn message="Hello World !" />,
  document.getElementById('app')
)
.hidden-block, .displayed-block {transition: opacity 0.3s}
.hidden-block {opacity: 0}
.displayed-block {opacity: 1}

CSS

JavaScript

useEffect

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const
  MouseTracking = props => {
    const [mousePosition, setMousePosition] = useState({x: 0, y: 0})
    
    useEffect(() => {
      const trackMousePosition = (e) => {
        const event = e || window.event
        setMousePosition({x: event.pageX, y: event.pageY})
      }
      document.addEventListener('mousemove', trackMousePosition)
      return () => document.removeEventListener('mousemove', trackMousePosition)
    }, [])
    
    return (
      <div onClick={() => props.closeMouseTracking(true)}>
        <h1>Mouse position</h1>
        <ul>
          <li>X : {mousePosition.x}</li>
          <li>Y : {mousePosition.y}</li>
        </ul>
      </div>
    )
  },
  Container = () => {
    const [isClosed, setClose] = useState(false)
    return !isClosed && <MouseTracking closeMouseTracking={setClose}/>
  }
ReactDOM.render(<Container/>, document.getElementById('app'))

useContext

import { createContext } from 'react'

const
  themes = {
    light: {
      color: '#000000',
      background: '#eeeeee'
    },
    dark: {
      color: '#ffffff',
      background: '#222222'
    }
  },
  ContextTheme = createContext(themes.light)

export { themes, ContextTheme }

ContextTheme.js

useContext

import React from 'react'

import { ContextTheme, themes } from './ContextTheme'
import ThemedButton from './ThemedButton'

export default () => {
  return (
    <>
      <ThemedButton/>
      <ContextTheme.Provider value={themes.dark}>
        <ThemedButton/>
      </ContextTheme.Provider>
    </>
  )
}

App.js

import React, { useContext } from 'react'

import { ContextTheme } from './ContextTheme'

export default () => {
  const theme = useContext(ContextTheme)
  return <button style={theme}>Mon bouton !</button>
}

ThemedButton.js

useReducer

import React, { useReducer } from 'react'
import ReactDOM from 'react-dom'

const
  reducer = (state, action) => {
    switch (action.type) {
      case 'increment':
        return {count: state.count + 1}
      case 'decrement':
        return {count: state.count - 1}
      default:
        throw new Error()
    }
  },
  Counter = () => {
    const [state, dispatch] = useReducer(reducer, {count: 0})
    return (
      <>
        <div>Total : {state.count}</div>
        <ul>
          <li onClick={() => dispatch({type: 'decrement'})}>-</li>
          <li onClick={() => dispatch({type: 'increment'})}>+</li>
        </ul>
      </>
    )
  }

ReactDOM.render(
  <Counter/>,
  document.getElementById('app')
)

useMemo

import React, { useMemo } from 'react'
import ReactDOM from 'react-dom'

const Product = props => {
    const priceWeightKg = useMemo(
      () => Math.round(props.price * 1000 / props.weight) / 100,
      [props.price, props.weight]
    )
    return (
      <ul>
        <li>Name : {props.name}</li>
        <li>Weight (gr.) : {props.weight}</li>
        <li>Price : {props.price}</li>
        <li>Price/Kg : {priceWeightKg}</li>
      </ul>
    )
  }

ReactDOM.render(
  <Product name="Sandwish" price={4.5} weight={300}/>,
  document.getElementById('app')
)

useRef

import React, { useState, useRef } from 'react'
import ReactDOM from 'react-dom'

const FormCustom = () => {
    const
      [value, setValue] = useState(''),
      input = useRef(null)
    return (
      <div>
        <p>{value}</p>
        <input type="text" ref={input}/>
        <button onClick={() => setValue(input.current.value)}>GET VALUE !</button>
        <button onClick={() => input.current.focus()}>FOCUS !</button>
      </div>
    )
  }

ReactDOM.render(
  <FormCustom/>,
  document.getElementById('app')
)

Quelques packages npm utiles

nextjs (https://nextjs.org/)

Server side rendering avec Nodejs et React
afin de réaliser des sites Internet isomorphiques.

npx create-next-app

reactn (CharlesStover/reactn)

Pour gérer un état global de l'application avec des hooks.

npm install reactn
import React, { useGlobal } from 'reactn';

export default () => {
  const [user, setUser] = useGlobal('user')
  
  return (
    <ul>
      <li>Nom : {user.name}</li>
      <li>Identifiant : {user.mail}</li>
      <li>Pseudo : {user.nickname}</li>
    </ul>
  )
}
import React, { useGlobal } from 'reactn';

export default () => {
  const [user, setUser] = useGlobal('user')
  return <input type="text" 
                defaultValue={user.name}
                onChange={(e) => setUser({...user, name:e.target.value})}/>
}

react-cookies

Pour gérer les cookies avec des hooks.

import React from 'react'
import { useCookies } from 'react-cookie'

export default () => {
  const [cookies, setCookie, removeCookie] = useCookies(['name'])
  return <input type="text"
                defaultValue={cookies.name}
                onChange={(e) => setCookie('name', e.target.value, {path: '/'})}/>
}
npm install react-cookie

(reactivestack/cookies)

react-geolocated

Pour utiliser l'API de géolocalisation du navigateur.

npm install react-geolocated
import React from 'react'
import { geolocated, geoPropTypes } from 'react-geolocated'
import ErrorMessage from './ErrorMessage'

const GeolocateStop = props => {
  if (!props.isGeolocationAvailable) {
    return <ErrorMessage message="Géolocalisation non supportée..."/>
  }
  if (!props.isGeolocationEnabled) {
    return <ErrorMessage message="Géolocalisation non activée..."/>
  }
  if (!!props.positionError) {
    return <ErrorMessage message="Erreur de géolocalisation..."/>
  }
  if (!props.coords) {
    return <ErrorMessage message="Recherche..."/>
  }
  return <h1>Votre position : Lat {props.coords.latitude}, Lng {props.coords.longitude}</h1>
}

GeolocateStop.propTypes = {...GeolocateStop.propTypes, ...geoPropTypes}
export default geolocated()(GeolocateStop)

(no23reason/react-geolocated)

Et bien plus !!

La communauté autour de React est très grande et active !!

Maintenant...

Entrainons-nous !!

Franck ALARY
github.com/DantSu
www.developpeur-web.dantsu.com

Slides

slides.com/dantsu/functional-components-hooks-
la-nouvelle-methode-react

Functional components & Hooks : La nouvelle méthode React

By Franck Alary

Functional components & Hooks : La nouvelle méthode React

  • 745