A JavaScript library to create user interfaces
Formateur: Fabio Ginja
@Ambient-IT
Each time user changes page force the download of a new HTML page. Such application would server-side-rendered.
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.
The most popular frameworks les plus populaires remain Angular, React & Vue.js.
"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."
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.
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
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 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).
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
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
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.
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 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, 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.
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)
)
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
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>
</>
)
}
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"/>
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>
)
}
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.
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.
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:
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} />
)
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} />
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>
)
}
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
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
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.
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:
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.
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>;
}
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 }
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";
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
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.
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?
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)
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.
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
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
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!
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 is an alternative for functional component to access the lifecycle of class component.
It takes two params:
import React, { useEffect } from 'react';
const SomeComponent = (props) => {
// Some code
useEffect(() => {
// Effect
return () => {
// Clean up effect
};
}, [input]);
// Some Code
return (
//Some JSX
)
}
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 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 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>
</>
)
};
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 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.
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>
)
}
<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?
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.
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.
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.
There are librairies to help us deal with forms (as React Hook Form or Formik).
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 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 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 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 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.
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.
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.
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;
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;
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"
},
...
}
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',
}
})
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);
});
});
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
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()
})
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.
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
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
Le store (état de notre application) est un simple objet javascript:
{ 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.
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.
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.
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
// 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.
// 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.
// 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
}
}
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:
// 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:
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.
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:
Pour résoudre notre problème, on va utiliser le middleware: 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
// 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).
// 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))
}
}
// 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 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 {
// ...
}
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>
);
}
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
Functional Component
const Learn = () => {...}
Utilise "this" pour accèder au state/props:
this.state.someVariable
this.props.someVariable
Accède directement aux props:
someState
props.someVariable
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
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) !
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...
// 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.
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
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.
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)
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)
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.