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:
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.
Possible types:
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:
- The first one is the function to execute when dependencies are updated
- 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
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.
- Installer l'extension sur votre navigateur (https://github.com/zalmoxisus/redux-devtools-extension)
- 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