React

A JavaScript library to create user interfaces

Formateur: Fabio Ginja
@Ambient-IT

Introduction to React

Multi Page Application

Each time user changes page force the download of a new HTML page. Such application would server-side-rendered.

Single Page Application

After a form submission, an API call would be made with Javascript. Our server will send data in JSON format annd our App will only update the specific part of the HTML. Client is the core of our app.

Frameworks Javascript

  • Vue.js - Alibaba, Tencent, Baidu
  • Angular - Forbes, Xbox, Parrot
  • React - Facebook, Netflix, Airbnb, Slack
  • Svelte - Cloudfare, Spotify, Ikea​
  • Ember.js - Linkedin, DigitalOcean
  • Polymer - Google Play Music, Coca-Cola
  • Backbone.js - Reddit, Amazon
  • Aurelia - seloger
  • Meteor - SFR
  • Mithril - Vimeo, Nike

The most popular frameworks les plus populaires remain Angular, React & Vue.js.

React - component based

"Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes."

"Build encapsulated components that manage their own state, then compose them to make complex UIs."

Example of component

A Component describe the HTML that will be rendered according to the provided params.

If any of the parameters change (name or description), HTML will be updated.

import React from 'react'

const Card = ({name, description}) => {
  return (
    <div>
      <span>Product: {name}</span>
      <p>Description: {description}</p>
    </div>
  )
}

export default Card

The code inside the return looks like HTML, but it's JSX... We will come back to that later on.

Hierarchy of components

Each component can call other components. Inn the end, our app will look like a tree of components.

import React from 'react'
import Card from './Card'

const Parent = () => {
  return (
    <div>
      <Card name="Game of thrones" description="Greats Books" />
      <Card name="The Lion King" description="Super movie" />
    </div>
  )
}

export default Parent

React and Virtual DOM

The DOM (Document Object Model) is an API that represents and interacts with any HTML or XML document.

A DOM node could be modified in javascript like so:

document.getElementsByTagName('p')[0].innerHTML = "Injected content in first <p>"

However, each access to our DOM to rewrite it is  resource consuming... React solution is to keep an inner representation of the DOM: the Virtual DOM

React and Virtual DOM

React updates the Virtual Dom at every render, but only apply the changes to the Real DOM only if it needs to be updated (values actually changed).

Create our first app

create-react-app

To create our React app, we could use the tool create by facebook: create-react-app (CRA)

npx create-react-app my-first-app --template typescript
cd my-first-app/
npm start

But vite is more performant to create a React application (faster builds, and faster Hot Module Reloading - HMR)

npm create vite@latest my-react-app -- --template react-ts

TypeScript with ESLint

For a better developer experience, detect typos and errors in our code,  and enforce code style in our project, we will typescript with eslint.

npm install --save-dev eslint
yarn add -D eslint

We will then initialize eslint:

npx eslint --init
▸ To check syntax and find problems
▸ JavaScript modules (import/export)
▸ React
? Does your project use TypeScript? ‣ Yes
✔ Browser
✔ Node
▸ JSON
? Would you like to install them now with npm? ‣ Yes

And install those packages:

npm install -D eslint-plugin-import @typescript-eslint/parser eslint-import-resolver-typescript
yarn add -D eslint-plugin-import @typescript-eslint/parser eslint-import-resolver-typescript

TypeScript with ESLint (2)

We need to modify .eslintrc file like so:

{
  "env": {
    "jest": true // add
  },
  "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"],
  "extends": [
    "plugin:react/jsx-runtime", // add
    "prettier" // add
  ],
  "settings": {
    "import/resolver": {
      "typescript": {}
    }
  }
}

We are now ready to install Prettier.

ESLint with Prettier

We can install Prettier to keep the same code formatting along all the files of our project:

npm install -D prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react-hooks@latest
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react-hooks@latest

Then create configuration file for Prettier:

touch .prettierrc

Then we add in our package.json:

{
  "semi": false,
  "tabWidth": 2,
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all",
  "jsxSingleQuote": true,
  "bracketSpacing": true
}

With our different rules:

{
  "scripts": {
    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx,json}'",
    "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx,json}'",
    "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,md,json}' --config ./.prettierrc"
  }
}

Entry Point

Entry point of our app will be index.js/main.js. It will inject our React app into index.html in div with id root.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

JSX

JSX

JSX, stands for JavaScript & XML, is an extension to JavaScript close to XML syntax.

We will write React components thanks to JSX:

const name = 'Fabio'
const element = (<h1>Bonjour, {name}</h1>)

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(element);

JSX isn't mandatory to write React components. Our JSX elements will be compiled by React into a regular Javascript function. It works as a syntaxic sugar.

JSX - createElement

Those two versions do the same thing:

const Button = ({name}) => {
  return <button>Send to {name}</button>
}

root.render(
  <Button name="Fabio" />
)

The components we create should always start by a uppercase.

const Button = ({name}) => {
  return React.createElement('button', {name}, `Send to ${name}`)
}

root.render(
  React.createElement(Button, {name: 'Fabio'}, null)
)

Rules: one node

We are allowed to return only one node:

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <h2>Profil</h2>
    <UserCard id={1} name="Fabio"></UserCard>
  )
}
// ⛔ This example will return an error

We will need to write:

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <div>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </div>
  )
}
// ✅ Valid

JSX

To return more than one node without adding a new parent node, we can use React.Fragment

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <React.Fragment>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </React.Fragment>
  )
}

Or it's shortcut:

import React from "react"
import UserCard from "./Usercard"
const Profile = () => {
  return (
    <>
      <h2>Profil</h2>
      <UserCard id={1} name="Fabio"></UserCard>
    </>
  )
}

JSX

Every node without a child can be self-closing.

<div>
    <span></span>
    <UserCard id={1} name="Fabio"></UserCard>
</div>
// Same as
<div>
    <span />
    <UserCard id={1} name="Fabio"/>
</div>

JSX is different from HTML...

Some keywords are reserved in JavaScript:

// HTML
<label class="bigLabel" for="someInput"/>

// JSX
<label className="bigLabel" htmlFor="someInput"/>

CSS

To write CSS in React, we have several solutions:

We can write inline css:

const image = 'someurl.com/img.jpg'
const divStyle = {
  color: 'blue',
  backgroundImage: `url(${image})`,
}

const HelloWorldComponent = () => (<div style={divStyle}>Hello World!</div>)

But it's better to write CSS in a module to scope it:

/* myComponent.module.css */

.red {
    color: red;
}
import styles from './myComponent.module.css'

const myComponent = () => {
  return (
    <p className={styles.red}>It will be red</p>
  )
}

Props

Props

Each component we create only accept one argument called: props. This argument will always be an object.

A prop is a property that the parent component pass to the child component.

The following example is a component using the name property passed by the parent component:

import React from 'react'

export const Welcome = (props) => {
  return (
    <p>
      Bonjour {props.name}
    </p>
  )
}

To use a Javascript variable in JSX, we need to use curly brackets.

Props (2)

We can pass several properties to a component. To do so, we choose an attribut name (at the left of the equal sign) followed by it's value (right of the equal sign) as we would in HTML.

<MyComponent propsA={valueA} propsB={valueC}/>

<UseCard name="Tony Start" hero="Iron Man"/>

⚠️ We should NEVER mutate the values of the props received by a parent component.

Passing props

ReactDOM.render(
  <Welcome name="Fabio" />,
  document.getElementById('root')
)

But also:

// Result of a function, a computed value
<Welcome name={"FABIO".toLowerCase()}/>
<Welcome id={33 + (25 * 100)}/>

// A variable that conntains a string, ann array, an object, etc...
const myName = "Fabio"
<Welcome name={myName}/>

// A reference of a function
<button onClick={() => { console.log("clicked !"); }}/>

We can pass any type as a property, like a string:

Props and Spread Operator

We can also use the spread operator to pass down multiple properties to a component:

const user = {
  name: "Fabio",
  gender: "male"
}

return (
  <Welcome {...user} />
)

Same as:

const user = {
  name: "Fabio",
  gender: "male"
}

return (
  <Welcome name={user.name} gender={user.gender} />
)

Props et default values

If we pass a props without any value,  by default, the boolean true would be assigned to it:

<SomeComponent visible />
// same as
<SomeComponent visible={true} />

Props.children

children  is a reserved property of the props object. It contains the children of our component:

const DisplayThreeTimesComponent = (props) => {
    return <div>
        {props.children}
        {props.children}
        {props.children}
    </div>
}


const ParentComponent = () => {
    return (
      <div>
          <Header />
          <DisplayThreeTimesComponent>
              <p>Hello !</p>
          </DisplayThreeTimesComponent>
      </div>
    )
}

Exercice: passing down props

Clone the following repository:

https://github.com/FabioReact/exercice-react

git clone https://github.com/FabioReact/exercice-react.git
cd exercice-react
npm install / npm i / yarn
npm start / yarn start

Loop

This is how we can loop into an iterable and return an array of JSX elements:

const names = ['John', 'Jack', 'James']
const arrayOfLi = names.map((name) => <li>{name}</li>)
return (
  <ul>
    {arrayOfLi}
  </ul>
)
// Ou encore
const names = ['John', 'Jack', 'James']
return (
  <ul>
    {names.map(name => <li>{name}</li>)}
  </ul>
)

Exercice on loops

keys

When we render an array of elements, React needs an unique way to identify each element (in case of a deletion or re-ordering).

const names = ['Tony', 'Steve', 'Natasha']
return (
  <ul>
    {names.map((name, index) => <li key={index}>{name}</li>)}
  </ul>
)

React uses those keys to identify which element appeared/disappeared/updated.

Documentation: https://react.dev/learn/rendering-lists

Caution, in this example we use the index of the array as keys... This should be avoided, and we should use a unique id as one that would be received from a database.

propTypes whitout TypeScript

import PropTypes from 'prop-types'

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    )
  }
}

Greeting.propTypes = {
  name: React.PropTypes.string
}

We can type props like so if we are still using Classes.

It will be verified at runtime.

Alternative with TypeScript

type Props = {
  name: string;
};

const Greeting = (props: Props): JSX.Element => {
  return (
    <h1>Hello, {props.name}</h1>
  );
};

Each prop should have a defined type.

If type is not correct, an error will arise in our IDE.

defaultProps

class Welcome extends React.Component {
  render() {
    return (
      <h1>Welcome, {this.props.name}</h1>
    )
  }
}

Welcome.defaultProps = {
  name: "Fabio"
}

Default values for props, with class components:

For functional component, we can use the basic features of ES6!

const Greeting = ({name = "Fabio"}) => {
    return <h1>Hello, {name}</h1>;
}

Structure of our application

Export

In order for a component to be usable elsewhere than in the file in which it was created, it must be exported.

However, there are many ways to export a component / a function:

Default export:

const MyComponent = () => {
  // ...
}

export default MyComponent

Named export (with/without alias):

const MyComponent = () => {}
const MySecondComponent = () => {}

export { MyComponent, MySecondComponent as MSComponent }

Only one default export per file is possible, but you can make as many named exports as you like:

export { function1 as default, function2, function3 }

Import

The function/component must then be imported in accordance with the method used to export it:

Default import:

import MyComponent from "./path/to/component";

Named Import:

import { MyComponent } from "./path/to/component";

It is also possible to make a default import followed by named imports:

import functionByDefault, { function2, function3 } from "./path/to/functions";

It's worth mentionning that you don't have to keep the original name, thanks to the default import.

import MyRenamedComponent from "./path/to/component";

We have to import the component by it's name. If we want to rename it, I can do so by using an alias.

import { MyComponent as MonComposantRenomme } from "./path/to/component";

File Structure

React has no opinion on the file structure we should adopt. This structure may therefore vary from one project, company, team to another.

You can, however, organize your project by functionality or by file type.

src 
│
└───hooks
│   │   useCustomHook.jsx
│   
└───components
│   │   Navbar.jsx
│   │   Header.jsx
│   │   Footer.jsx│  
│
└───pages
│   │   Home.jsx
│   │   Blog.jsx

Data Flow - Data & events

Data Flow

Props only go in one direction: from parent components to children.


Consequently, child components cannot directly influence the display of parents.

 

There is, however, one (indirect) way of doing this:

 

parents can pass, as  a prop, a reference to a function to the child component. Once called, that callback will be able to modify the parent component's state.

Data Flow

Data Flow

Most of the application's components will be very simple.

 

We'll have one or a few "container" components, at the root of the application, which will have to connect in one way or another to the rest of the application....

 

Depends on the solution chosen: homemade? Redux? React Context?

Class Component Lifecycle
(Déprécié)

Introduction

On peut diviser le cycle de vie d'une composant en 3 étapes successives:

1. Création (Mount)

2. Mise à jour (Update)

3. Destruction (Unmount)

Création (Mount)

Phase d'initialisation du composant.

Le composant ainsi que tous ses enfants sont montés dans l'UI.

A ce stade, ni les props, ni le state ne sont encore définis.

Création (Mount)

class MyComponent extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  static getDerivedStateFromProps(nextProps, prevState)
  {
    if (true) {
      return someNewState
    }
    return null
  }

  render() {
    // JSX
  }
 
  componentDidMount() {
    // Code After-render
  }
  
}

initialisation du state

rafraîchit l'UI

Le composant est désormais monté, on peut maintenant faire des requêtes à la bdd

met à jour le state en fonction des props

Mise à jour (Update)

class MyComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Renvoie "true" par défaut
  }

  
  
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
	// Utilise si on a besoin de faire
	// des calculs selon l'état précédent
  }
 

  componentDidUpdate() {
    // Après le re-render
  }

  
  
  render() {
    // re-render
  }
  
}

Retourne un booléen, permet d'éviter le rafraîchissement inutile du composant

appelée après que le nouveau render ai eu lieu

rafraîchit l'UI

permet de capturer des infos du DOM courant

Destruction (Unmount)

class MyComponent extends Component {
  componentWillUnmount() {
	// Code unsubscribe
  }
}

Le composant n'est plus dans l'UI, endroit idéal pour se désabonner à d’éventuels événements.

Si vous ne le faites pas, vous vous exposez à des fuites mémoires.

Attention!

React Hooks

useState

import React, { useState } from 'react';

const Count = (props) => {
    const initialCount = 0; // Can be an array [], an object {},
    // a string, a number, a boolean
    const [count, setCount] = useState(initialCount);
    return (<>
      Total : {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>);
}

export default Count;

useState return an array. The first value will be the current state (getter), and the second value will be a function used to update the state (setter).

Since React v16.8, functional components can also have a state:

useEffect

useEffect is an alternative for functional component to access the lifecycle of class component.

It takes two params:

  1. The first one is the function to execute when dependencies are updated
  2. The second is an array of dependencies
import React, { useEffect } from 'react';

const SomeComponent = (props) => {
   	// Some code
    useEffect(() => {
      // Effect
	  return () => {
         // Clean up effect
	  };
    }, [input]);
   	// Some Code
    return (
      //Some JSX
    )
}

useLayoutEffect

useLayoutEffect has the same signature as useEffect. The difference between them is when the effects are executed.

useLayoutEffect is executed synchronously after an update, i.e. before the browser repeats the page.

Lorem

import React, { useLayoutEffect } from 'react';

const SomeComponent = (props) => {
   	// Some code
    useLayoutEffect(() => {
      // LayoutEffect
	  return () => {
         // Clean up effect
	  };
    }, [input]);
   	// Some Code
    return (
      //Some JSX
    )
}

useRef

useRef is the alternative of createRef to create a reference in a functional component.

The object returned by useRef will persist for the lifetime of the component. References are particularly useful for accessing element of our components.

import React, { useRef } from 'react';

const SomeComponent = (props) => {
  const usernameRef = useRef(null);
  const login = () => {
    const username = usernameRef.current.value;
    // Some code...
  }
  return (
    <>
      <input ref={usernameRef} type="text" id="username" />
      <button onClick={login}>Submit</button>
    </>
  )
};

useReducer

useReducer is an alternative of multiple useState. It is sometimes easier to have a single reducer than many uses of useState.

import React, { useReducer } from 'react';

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return {counter: state.counter + 1};
    case 'decrement':
      return {counter: state.counter - 1};
    default:
      throw new Error("Incorrect action type");
  }
}

const Counter = (props) => {
  const initialState = {counter: 0};
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <p>Compteur: {state.counter}</p>
      <button onClick={() => dispatch({type: 'increment'})}>Increment</button>
      <button onClick={() => dispatch({type: 'decrement'})}>Decrement</button>
    </>
  )
};

useTransition

Since React 18, it is possible to use concurrent mode in order to prioritize certain UI updates.

useTransition allows you to specify some UI updates as non-urgent and those could be interrupted. This is useful when the UI need to be fast and some updates may need to be delayed (e.g. a filter).

import React, { useTransition } from 'react';

const MyComponent = (props) => {
  const [isPending, startTransition] = useTransition()
  
  const someEventHandler = (event) => {
    startTransition(() => {
      // Every updates here is declared as non-urgent and can be interrupted
      setValue(event.target.value);
    });
  }

  return (...)
};

useDeferredValue

useDeferredValue is similar to useTransition, but is used in a child component whose information comes from the parent. If a more urgent update is made, React will return the previous value and only update non-urgent updates afterwards.

import React, { useDeferredValue } from 'react';

const MyComponent = ({ list }) => {
  const deferredValue = useDeferredValue(list);

  return (
    <>{deferredValue.map(el => <OtherComponent info={el} />)}</>
  )
};

To take full advantage of useDeferredValue, we'll also need to memoize our component.

Conditional Rendering

Conditional Rendering

function HelloUser(props) {
  if (!props.name) return (<p>Hello visitor</p>)
  return (<p>Hello {props.name}</p>)
}

It is possible to display a component, depending on a given condition.

The first solution is to use an if statement:

Or the ternary operator:

function HelloUser(props) {
  return (<p>Hello {props.name ? props.name: 'visitor'}</p>)
}

Or to store the component in a variable:

function HelloUser(props) {
  let component = null
  if (props.name) component = (<p>Hello {props.name}</p>)
  else component = (<p>Hello visitor</p>)

  return component
}

&& - ||

function HelloUser(props) {
  return (
    <p>Hello gamer</p>
    {props.points && <p>You have {props.points} points so far. Good job!</p>}
  )
}

Logical operator && returns what will be to the right of the latter if what is on the left is truthy:

Logical operator || returns the first element that will be truthy:

function HelloUser(props) {
  return (
    <p>Hello gamer</p>
    <p>You have {props.points || 0} points so far.</p>
  )
}

Forms

Forms

<input
  type="text"
  value="My value"
/>

In a form, you can specify the value of an input. This is called Controlled input.

To do this, we specify the value property.

But how can it be updated?

Forms (Controlled) - Class

  constructor(props) {
    super(props);
    this.state = {inputValue: "My default value"};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleChange(event) {
    this.setState({inputValue: event.target.value});
  }
  handleSubmit(event) {
    console.log('The submitted value was ' + this.state.inputValue);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type="text"
          value={this.state.inputValue}
          onChange={this.handleChange}
        />
        <button type="submit">Submit</button>
      </form>
    );
  }

We have to subscribe to every change with the onChange event, and then update the state with the value of the captured event.

Forms (Controlled) - Hooks

  const [inputValue, setInputValue] = useState('')
  
  const handleChange = (event) =>  {
    setInputValue(event.target.value)
  }
  
  const handleSubmit = (event) => {
    event.preventDefault()
    console.log('The submitted value was ' + inputValue)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
      />
      <button type="submit">Submit</button>
    </form>
  );

We have to subscribe to every change with the onChange event, and then update the state with the value of the captured event.

Forms (Uncontrolled) - Class

constructor(props) {
  super(props);
  this.inputRef = React.createRef();
}

handleSubmit(event) {
  console.log('we got the value ' + this.inputRef.current.value);
  event.preventDefault();
}

render() {
  return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <input 
          type="text"
          defaultValue="A default value"
          ref={this.inputRef} />
        <button>OK</button>
      </form>
  );
}

We don't specify the value attribute (only defaultValue), and we use a ref to retrieve the value of the input.

Forms: Which one to choose?

There are librairies to help us deal with forms (as React Hook Form or Formik).

Optimisations

React Lazy

Permet d'afficher du JSX ou un autre component en attendant le chargement du 'lazy' component.

import React, { Suspense, lazy } from 'react';
import Profile from './components/Profile';

const User = lazy(() => import('./components/User'));

const App = () => (
  <React.Fragment>
    <Profile />
    <Suspense fallback={<div>Loading...</div>}>
      <User />
    </Suspense>
  </React.Fragment>
);

Pure Component - Classes

Pure Components est une alternative à l'utilisation de la méthode shouldComponentUpdate des class components.

Ce dernier effectue un shallow check du state et des props. S'il n'y a eu aucun changement, le composant ne sera pas mis à jour.

import React, { PureComponent} from 'react';

class SomeComponent extends PureComponent {
    // Some Code
    render() {
      return (
      	// Some JSX
      );
    };
};

Très efficace pour éviter des rendu inutiles.
N'utiliser que PureComponent si les props ou le state du parent peuvent vraiment provoquer un render inutile du component enfant.

React Memo

React Memo est une alternative à l'utilisation de la méthode shouldComponentUpdate pour les functional components.

Même objectif que les PureComponent.
Attention, n'effectue qu'une shallow comparaison et peut ne pas détecter des changements à l'intérieur d'un tableau, objet (reference types).

import React from 'react';

const SomeComponent = (props) => {
    // Some Code
    return (
      // Some JSX
    );
};

export default React.memo(SomeComponent);

useCallback

useCallback permet de renvoyer une fonction de rappel mémoïsée. Pour cela on doit passer la fonction que l'on souhaite mémoïser, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:

import React, { useCallback } from 'react';

const SomeComponent = (props) => {
  // ...
  const memoizedCallback = useCallback(
    () => {
      doSomething(a, b);
    },
    [a, b],
  );
  // ...
};

Cela permet d'éviter des rendu inutiles lorsqu'on passe une fonction à un composant utilisant shouldComponentUpdate, React.memo, ou PureComponent.

useMemo

useMemo permet de renvoyer une valeur mémoïsée. Pour cela on doit passer la fonction dont on souhaite obtenir le résultat, ainsi qu'un tableau de dépendance indiquant les entrées de notre fonction:

import React, { useMemo } from 'react';

const SomeComponent = (props) => {
  // ...
  const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  // ...
};

Si les inputs n'ont pas changé, alors on gardera la valeur précédente du retour de la fonction. 

Cela est très utile lorsqu'on fait appel à une fonction qui fait des calcul coûteux.

Si on ne fournis aucun tableau, une nouvelle valeur sera calculée à chaque appel.

React Context

React Context

Le contexte sert à partager une valeur entre différents composants quel que soit la distance entre ces derniers.

La première étape est de créer un contexte.

// theme-context.js
import{ createContext } from 'react';

const ThemeContext = createContext({
    foreground: "#000000",
    background: "#eeeeee"
});

export default ThemeContext;

Les valeurs renseignées dans la méthode createContext ne sont pas des valeurs pas défaut. Celles-ci seront néanmoins utiles pour de l'autocompletion lors de l'utilisation du context.

Vous pouvez créer autant de contexte différent que vous le souhaitez.

React Context - Provider

La deuxième étape sera de preciser quelle partie de l'application aura accès a ce contexte. Seuls les enfants de la balise Provider aura accès ensuite au contexte donné.

// App.jsx
const App = () => {
  return (
    <>
      <ThemeContext.Provider value={{
        foreground: "#ffffff",
        background: "#222222",
      }}>
        <Home />
      </ThemeContext.Provider>
      <Footer />
    </>
  );
}

Dans cet exemple, le footer n'aura pas accès au contexte du thème.

React Context - useContext

La derniere étape sera de consommer notre contexte. Il existe deux façon de le faire. Cette dernière reste la plus simple:

// ThemeButton.jsx
import ThemeContext from "./context/theme-context";

const ThemedButton = () => {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      Je suis stylé par le contexte de thème !
    </button>
  );
}

export default ThemeContext;

Ou encore de créer un custom hook:

import ThemeContext from "./context/theme-context";

const useTheme = () =>  useContext(ThemeContext);

export default useTheme;

React Context - Consumer

On peut aussi consommer le contexte via la balise Consumer comme suit:

// ThemeButton.jsx
import ThemeContext from "./context/theme-context";

const ThemedButton = () => {
  return (
    <ThemeContext.Consumer>
      {(theme) => (
        <button style={{
          background: theme.background,
          color: theme.foreground
        }}>
          Je suis stylé par le contexte de thème !
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeContext;

Unit Test

Installation de vitest

Afin de faire mettre en place des test unitaires sur un projet React mis en place avec Vite, on va devoir installer Vitest, 

npm install -D vitest

Une fois cette dépendance ajoutée, on peut ajouter à nos scripts:

{
  ...
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest"
  },
  ...
}

Configuration

On ajoute ensuite ceci au fichier vite.config.ts afin de créer notre configuration de test:

/// <reference types="vitest" />
/// <reference types="vite/client" />

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
    test: {
      globals: true,
      environment: 'jsdom',
    }
})

Premier ficher de test

On peut maintenant écrire notre premier test.

describe permet de créer un bloc qui regroupe plusieurs tests liés.

it, ou test décrit un test. Le premier argument décrit ce qu'on cherche à tester, et le second est la fonction qui contient nos assertions.

import { describe, it, test, expect } from 'vitest';

describe('something truthy and falsy', () => {
  it('should be true', () => {
    expect(true).toBe(true);
  });

  test('false to be false', () => {
    expect(false).toBe(false);
  });
});

Installation de testing-library

On va enfin installer testing library qui nous permet de tester une application React

npm install --save-dev jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

Création d'un fichier de test

On peut maintenant utiliser les fonction de testing libraby pour tester notre composant:

import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import Fetch from './fetch'

test('loads and displays greeting', async () => {
  // render permet de charger notre composant dans notre test
  render(<Fetch url="/greeting" />)

  // userEvent permet de simuler des actions utilisateur
  await userEvent.click(screen.getByText('Load Greeting'))
  // getBy throw une erreur si le noeud n'est pas trouvé contrairement à findBy
  await screen.findByRole('heading')

  // ASSERT
  expect(screen.getByRole('heading')).toHaveTextContent('hello there')
  expect(screen.getByRole('button')).toBeDisabled()
})

Redux - Concepts

Principes

Les 3 principes de redux sont les suivants:

Single source of truth

L'état de toute notre application est stocké dans un seul objet store.

State is read-only

Le seul moyen de changer le state est via l'émission (dispatch) d'une action. Une action décrit ce qu'il vient de se passer.

Changes are made with pure functions

L'état de notre application (store) sera changé à partir de fonctions pures: les reducers.

Une fonction pure retourne toujours le même output pour un input donné, et ne doit pas avoir d'effets de bord.

Fonctionnement

Le store contient l'état de notre application. Pour changer cette état, on va dispatch des actions. Ces actions vont être traités par un reducer qui va déterminer l'état futur de notre application en fonction de l'action qui lui a été transmise.

View

Action

Dispatch

Reducer

Store

Fonctionnement

Store

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

Le store (état de notre application) est un simple objet javascript:

Actions

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

Les actions sont également des objets javascript. Cependant celles-ci doivent impérativement contenir la propriété type (ce dernier étant une string).

D'autres propriétés peuvent lui être ajoutés, elles sont optionnelles dans l'absolu mais peuvent être nécessaire selon le traitement que vous en faîte dans le reducer.

Actions Creators

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}

Les actions creators sont des fonctions qui retournent une action.

Les actions creators vont notamment nous être utile pour la mise en place de requêtes asynchrones.

Reducer

const initialState = {};

function rootReducer(state = initialState, action) {
  // On mettra en place ici un switch/case pour une faire un traitement particulier
  // selon le type de l'action qui aura été passé en argument lors du dispatch
  // et ainsi retourner le nouvel état de l'application
  return state
};

Un reducer est une fonctions qui a la spécificité de devoir être pure.

Le reducer doit prendre comme premier argument le state courant (qu'il faudra initialiser au départ) et pour second l'action qui lui sera dispatch.

Redux avec React (legacy)

Packages

npm install --save redux react-redux
OU
yarn add redux react-redux

Pour installer redux dans notre application react, il nous faudra installer les deux packages suivant:

redux et react-redux

Le Provider

// src/index.js
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './store/reducer';

const myStore = createStore(rootReducer);

ReactDOM.render(
  <Provider store={myStore}>
    <App />
  </Provider>,
  document.getElementById('root')
);

On souhaite que toute notre application react ai accès à redux, pour cela on va mettre notre composant Provider (fournit par react-redux) à la racine de notre application:

Pour créer notre store, on fait appel à la fonction createStore de redux. Pour ce faire on doit passer en paramètre à createStore notre reducer.

Le Reducer

// src/store/reducer.js
import * as actionTypes from './actionTypes'

const initialState = {
  todos: []
};

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case actionTypes.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      }
    default:
      return state
  }
}

Ici, on importe les action types d'un autre fichier js. Il faut alors le créer.

Les Action types

// src/store/actionTypes.js
export const ADD_TODO = "ADD_TODO"

Par convention, les types des actions sont en uppercase. Le fait de stocker nos types d'actions dans une variable permet de générer des erreurs lors de la compilation si on fait une faute de frappe (lors de la création d'une action ou lors de sa lecture dans le reducer).

A partir de cette étape, on a créer avec succès notre store. On va pouvoir désormais connecter nos composants avec le store.

// src/store/actionCreators.js
import * as actionTypes from './actionTypes'
export const addToDo = (text) => {
  return {
	type: actionTypes.ADD_TODO,
	text: text
  }
}

Connecter nos composants

On va maintenant voir comment "connecter" un de nos composant avec redux. Une fois un composant connecté, il recevra via ses props ce que l'on aura déterminé.

On recevra deux types de props:

  • des données provenant du store, que l'on va pouvoir récupérer via la fonction mapStateToProps (va injecter dans nos props le state que l'on aura précisé de récupérer)
  • des fonctions callback, pour la mise à jour du store, que l'on pourra récupérer via mapDispatchToProps

Connecter nos composants

// src/pages/ToDoComponent.js
import React from "react"
import { connect } from "react-redux"
import { addToDo } from "../store/actionCreators.js"

const ToDoComponent = props => {
  // ...
}
// mapStateToProps reçoit comme paramètre le store actuel de redux
const mapStateToProps = state => {
  return {
    todos: state.todos // props.todos (type []) sera accessible dans notre component
  }
}
// mapStateToProps reçoit comme paramètre la fonction dispatch de redux
const mapDispatchToProps = dispatch => {
  return {
    onAddToDo: (text) => dispatch(addToDo(text)) // props.onaddTodo (type fn) sera  aussi accessible
  }
}
// connect permet de connecter ce que l'on vient de définir à notre composant
export default connect(mapStateToProps, mapDispatchToProps)(ToDoComponent)

Afin de connecter notre composant, il faut procéder comme suit:

Redux DevTools

import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducer';
import someMiddleware from './someOMiddleware';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
    reducer,
    composeEnhancers(applyMiddleware(someMiddleware))
)

Redux DevTools est une extension que l'on peut installer sur son navigateur afin d'avoir des outils de développement/debug.

  1. Installer l'extension sur votre navigateur (https://github.com/zalmoxisus/redux-devtools-extension)
  2. Modifier l'index.js

Redux asynchrone (legacy)

Asynchrone et redux-thunk

npm install --save redux-thunk
OU
yarn add redux-thunk

Jusqu'ici, tout est synchrone. Les dispatch font appel au reducer et ce dernier doit être pure...
Comment faire donc de l'asynchrone afin de faire un appel à la base de données?

Dès que l'on travaille avec un API, on enfreint les règles d'une fonction pure car:

  • on a un effet de bord
  • notre réponse est forcément asynchrone

Pour résoudre notre problème, on va utiliser le middleware: redux-thunk

redux-thunk

redux-thunk est une middleware qui va se positionner entre le dispatch de l'action et notre reducer, ce qui lui permet d'effectuer un traitement particulier. On va faire ça en dispatchant un nouvel action creator qui pourra être asynchrone grâce à redux-thunk.

Store

Reducer

Dispatch

Async Action

View

API call

Dispatch

Action

Mise en place

// src/index.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk'
import { Provider } from 'react-redux';
import rootReducer from './store/reducer';

const withMiddleware = applyMiddleware(thunkMiddleware)
const myStore = createStore(rootReducer, undefined, compose(withMiddleware));
ReactDOM.render(
  <Provider store={myStore}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Une fois notre package redux-thunk installé, on va le mettre en place sur notre projet:

On fait appel à notre thunkMiddleware, et on utilise la fonction applyMiddleware de redux afin de l'appliquer, applyMiddleware peut prendre autant de middleware en arguments que besoin.

compose de redux est une fonction afin de composer avec nos (possibles) différents enhancers (withMiddleware est un enhancer).

Async Action types

// src/store/actionTypes.js
export const ADD_TODO = "ADD_TODO"

On peut maintenant créer des action creators asynchrones. Ces derniers pourront par la suite appeler des action creators à travers la fonction dispatch.

// src/store/actionCreators.js
import * as actionTypes from './actionTypes'
export const addToDo = (text) => {
  return {
	type: actionTypes.ADD_TODO,
	text: text
  }
}

export const asyncAddToDo = (text) => {
  return dispatch => {
    // Exécuter de l'asynchrone ici...
    await response = fetch("URL")
    dispatch(addToDo(text))
  }
}

Dans notre composant...

// src/pages/ToDoComponent.js
import { asyncAddToDo } from "../store/actionCreators.js"

const mapDispatchToProps = dispatch => {
  return {
    onAddToDo: (text) => dispatch(asyncAddToDo(text))
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(ToDoComponent)

Le composant devra désormais uniquement appelé notre action creator asynchrone.

Class Components
(Déprécié)

Première Syntaxe: Class

class App extends React.Component

Ou encore

import React from 'react'

class MyComponent extends React.Component {
  constructor(props) {
      super(props);
      // ...
  }

  someCustomMethod(){
    // Code
  }    

  render() {
    return (<div>
      // Some Content
    </div>)
  }
}
import React, { Component } from 'react'

class MyComponent extends Component {
  // ...
}

Seconde Syntaxe: 

Les "functional components", la syntaxe est plus succincte, mais son comportement est cependant différent (cycle de vie). Se résume à écrire la méthode render() en prenant les props comme argument.

const MyComponent = (props) => {
    return (
      <div>
        { (...) }
      </div>
    );
}

Quelle syntaxe choisir?

Class Component vs Functional Component

Utilisez une classe lorsque vous avez besoin d'un state / lifecycle hooks et que vous ne pouvez pas utiliser les React Hooks (v16.8), sinon, utilisez un Functional component.

Class Component

class Learn extends Component

  • Possède un state
  • Accès au cycle de vie

Functional Component

const Learn = () => {...}

  • Possède un state depuis v16.8
  • Cycle de vie depuis v16.8

Utilise "this" pour accèder au state/props:

    this.state.someVariable

    this.props.someVariable

Accède directement aux props:

    someState

    props.someVariable

State

Le state est un objet qui représente un état interne du composant.

 

Lorsque une valeur dans le state est "mise à jour", alors l'application de met à jour.

 

Un usage serait un composant dont l'apparence changerait en fonction d’événements internes, et qui n'impacte pas le fonctionnement des composants parents.

Exemple: un carrousel

this.state

Il est uniquement possible d'y accèder de cette manière dans la syntaxe de classe.

this.state.someProperty pour lire les valeurs

this.setState({ someProperty : "something "}) pour setter les valeurs

this.setState() merge intelligemment (mais shallow) avec le valeurs courantes, pas besoin de tout redéfinir.

On ne doit jamais setter de cette de cette façon:

this.state = (...); 

...sauf dans le constructeur (phase d'initialisation du state) !

this.state

class TogglableImage extends React.Component {
  constructor(props) {
    super(props);
    this.state = {displayImage: true};
  }

  toggleImage() {
    this.setState({
      displayImage : !this.state.displayImage
    });
  }

  render() {
    return (
      <div>
        <button onClick={this.toggleImage.bind(this)}>Show/hide the image</button>
        {this.state.displayImage && <img src={logo}/>}
      </div>
    );
  }
}

Attention, dans cet exemple displayImage prend la valeur inverse de this.state.displayImage. Cette façon de procéder n'est pas correcte, on vas voir pourquoi...

this.setState()

// Peut se tromper
this.setState({
  counter: this.state.counter + 1
})


// Syntaxe toujours correcte !
// Reçoit le state courant et les props courantes au moment
// où l'update sera appliqué
this.setState((prevState, props) => ({
  counter: prevState.counter + 1
}))

Les setState() étant asynchrones, on n'a aucune certitude que la valeur précédente est bien celle à laquelle on s'attend.

Attention donc si vous dépendez des props courantes ou du state courant.

Attacher des events handler

class SayHello extends React.Component {

  handleClick() {
    // KO : "this" ici n'est pas la classe !
    this.setState({})
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Problème : perte du this

Attacher des events handler

class SayHello extends React.Component {

  handleClick() {
    // Ok
    this.setState({})
  }

  render() {
    // .bind(this) force à transmettre le this
    return (
      <button onClick={this.handleClick.bind(this)}>
        Say hello
      </button>
    );
  }
}

Solution 1 : .bind(this) à chaque usage

Non optimale car une fonction supplémentaire est appelée à chaque clique.

Attacher des events handler

class SayHello extends React.Component {

  constructor(props){
    super(props);
    // on bind à this et on écrase
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // OK
    this.setState(...)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Solution 2 : .bind(this) dans le constructor (la plus courante)

Attacher des events handler

class SayHello extends React.Component {

  constructor(props){
    super(props);
  }

  handleClick = () => {
    // OK
    this.setState({});
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Say hello
      </button>
    );
  }
}

Solution 3 : utiliser une arrow function (pas de changement de contexte avec cette dernière)

refs

constructor(props) {
    super(props);
    this.myRef = React.createRef();
}

componentDidMount(){
    // création d'une chart avec ChartJS
    new Chart(this.myRef.current, {
        (...)
    });
}


render() {
    // On utilise le `ref` callback pour stocker la référence vers
    // le noeud DOM dans un champ custom du this
    return (
      <div ref={this.myRef} />
    );
}

Une ref nous permet d'obtenir une référence vers un noeud du DOM du composant, afin d'en faire un usage spécifique.

React v18 (en)

By AdapTeach

React v18 (en)

Slides de formation ReactJS en anglais

  • 169