Formation de 3 jours
Écosystème JavaScript et découverte de React
React
Les composants en long et en large
Technique avancé
Routing, les effets de bords, ...
# Programme
# Programme
Demandez-moi de l'aide !
Dispo sur https://berthelot.io
Je navigue de formation en formation
7 ans d'expériences très diverses...
2 SSII, puis indépendant (WeFacto).
JavaScript
JavaScript ?
Créez une page Web, pour l'instant Vide.
Cette page charge un fichier JS et aussi ceci :
<!-- Votre page HTML -->
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<!-- Votre page HTML -->
# React
# React
Facebook !
# React
# React
- Stable
- Stable
- JSX
- Assez performant
- Simple !
- Progressive Framework ou Lib
# React
- Angular
- Vue.JS
- Là
<div id="my-react-application"></div>
# React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(React.createElement('h1', {}, 'Hello world'));
Démarrer React pour afficher votre nom sur la page Web
<div id="my-react-application"></div>
# React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(React.createElement('h1', {}, 'Hello world'));
# React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(
React.createElement('main', {},
[
React.createElement('header', {},
React.createElement('h1', {}, 'Hello world')
),
React.createElement('section', {}, 'lorem ipsum')
])
, domContainer);
# React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(
React.createElement('main', {},
[
React.createElement('header', {},
React.createElement('h1', {}, 'Hello world')
),
/** ... **/
React.createElement('section', {},
React.createElement('article', {}, [
React.createElement('p', {}, 'lorem ipsum'),
React.createElement('p', {}, 'lorem ipsum')
]),
React.createElement('article', {}, [
React.createElement('p', {}, 'lorem ipsum'),
React.createElement('p', {}, 'lorem ipsum')
])
)
/** ... **/
])
, domContainer);
# React
const domContainer = document.querySelector('#my-react-application');
const root = ReactDOM.createRoot(domContainer);
root.render(
<main>
<header>
<h1>Hello world</h1>
</header>
{/** ... **/}
<section>
<article>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</article>
<article>
<p>lorem ipsum</p>
<p>lorem ipsum</p>
</article>
</section>
{/** ... **/}
</main>
, domContainer);
Outillage JavaScript ?
- NVM
- NPM
- GIT
- JEST
- Babel / SWC
- ESLINT
- PRETTIER
- Installer React
- ... ?
Create React App
Initialisez le projet avec Create React App
Observez le code et les outils qui ont été généré.
Changez le contenu de la Page pour afficher votre nom.
<LikeButton></LikeButton>
ou
<LikeButton />
# Template
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
return <button>O j'aime</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return <button>{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}</button>;
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button" className="btn-primary">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className="btn-primary" style={{color: liked ? 'red' : 'grey'}}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Template
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Créez la structure HTML de votre arène de Pokémon.
Une arène se compose de deux Pokémons qui s'affrontent.
Pour l'instant, l'écran est figé.
Il n'y a qu'un seul composant Arène.
# Tests
# Tests
# Tests
Live-Coding
import { render, screen } from "@testing-library/react"
import { LikeButton } from "./likeButton"
describe('LikeButton', () => {
it("should display 0 like", () => {
render(<LikeButton />)
expect(screen.getByText('0 like')).toBeVisible()
})
})
Réparez vos tests !
# Props
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
const liked = true;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton></LikeButton>
# Props
Composant
Vue
// like.component.jsx
const LikeButton = (props) => {
const nbOfLike = props.nbOfLike;
const liked = props.liked;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked={true} nbOfLike={1} />
Propriétés
# Props
Composant
Vue
// like.component.jsx
const LikeButton = (props) => {
const nbOfLike = props.nbOfLike;
const liked = props.liked;
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked nbOfLike={1} />
Propriétés
# Props
Composant
Vue
// like.component.jsx
const LikeButton = ({
nbOfLike,
liked
}) => {
return (
<button type="button" className={`btn-primary ${liked ? 'btn-liked' : ''}`}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton liked nbOfLike={1} />
Propriétés
Séparez en plusieurs composants l'arène.
Passez des données entre les composants.
Et effets de bords
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const nbOfLike = 0;
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
<!-- Composant Parent -->
<LikeButton />
Propriétés
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const NbLikeState = React.useState(0);
return (
<button type="button">
{NbLikeState[0]} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# State & effects
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# State
Tout ce qui est dans un React.useState doit être totalement indépendant des autres useState.
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setEllapsedTime((ellapsedTime) => ellapsedTime + 1);
}, 1000)
}, [])
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
# State & effects
Composant
Vue
const Timer = () => {
const [ellapsedTime, setEllapsedTime] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => {
setEllapsedTime((ellapsedTime) => ellapsedTime + 1);
}, 1000)
return () => {
clearInterval(interval);
}
}, [])
return <span>Il s'est passé {ellapsedTime} secondes.</span>;
}
Propriétés
State
Les Pokémons s'attaquent maintenant à raison d'une attaque (ou tour) par seconde.
(Pour l'instant seul les points de vies changent)
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button" onClick={() => setNbOfLike(nbOfLike++)}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
# Évenement
Composant
Vue
// like.component.jsx
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
Propriétés
State
const Tweet = () => {
const [nbOfLikes, setNbOfLikes] = useState(0);
return <LikeButton nbOfLikes={nbOfLikes} onLike={() => setNbOfLikes(nbOfLikes++)} />
}
# Évenement
L'empathie !
# Évenement
import { fireEvent, render, screen } from '@testing-library/react';
it('should display 1 like when cliking on the button', () => {
render(<LikeButton />);
screen.fireEvent(screen.getByRole('button'))
expect(screen.getByText('1 like')).toBeVisible()
})
# Évenement
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'
it('should display 1 like when cliking on the button', async () => {
render(<LikeButton />);
await userEvent.click(screen.getByRole('button'));
screen.getByText('1 like');
})
Ajoutez un boutton Play/Pause.
Par défaut, le combat est en pause.
Validation et valeurs par défault
const LikeButton = ({
onLike = () => {},
nbOfLike = 0
}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Props 2
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
LikeButton.defaultProps = {
onLike: () => {},
nbOfLike: 0
}
# Props 2
import PropTypes from 'prop-types';
const LikeButton = ({onLike, nbOfLike}) => {
return (
<button type="button" onClick={() => onLike()}>
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
LikeButton.defaultProps = {
nbOfLike: 0
}
LikeButton.proptypes = {
onLike: PropTypes.func.isRequired,
nbOfLike: PropTypes.number
}
# Props 2
Boucles, affichage conditionnel, ...
const Tweets = () => {
const datas = [/* ... */]
return (
<React.Fragment>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</React.Fragment>
)
}
# Tips
const Tweets = () => {
const datas = [/* ... */]
return (
<>
<Tweet data={datas[0]}/>
<Tweet data={datas[1]}/>
</>
)
}
# Tips
const Tweets = () => {
const items = [
{id: 1, price: 10_00},
{id: 2, price: 150_00}
]
return (
<ul>
{items.map((item) => {
return <li>{item.price}</li>;
})}
</ul>
)
}
# Tips
const Tweets = () => {
const items = [
{id: 1, price: 10_00},
{id: 2, price: 150_00}
]
return (
<ul>
{items.map((item) => {
return <li key={item.id}>{item.price}</li>;
})}
</ul>
)
}
# Tips
Important !
Fait l'objet d'une règle ESLint
const Tweets = () => {
const tweets = [
{id: 1, message: 'coucou' /* ...*/ },
{id: 2, message: 'un thread !' /* ...*/ }
]
return (
<section>
{tweets.map(tweet => {
return <Tweet key={tweet.id} tweet={tweet}/>;
})}
</section>
)
}
# Tips
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{
isUserTweet ?
<EditButton /> :
null
}
</article>
)
}
# Tips
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet && <EditButton />}
</article>
)
}
# Tips
const Tweets = ({
isUserTweet
}) => {
return (
<article>
{/* ... */}
{isUserTweet ?? <EditButton />}
</article>
)
}
# Tips
const Parent = () => {
return (
<Modal>
<h2>lorem ipsum</h2>
<button>close</button>
</Modal>
}
const Modal = ({children}} => {
return (
<div className="modal">
{children}
</div>
)
}
# Tips
# Tips
const TweetPage = () => {
const tweets = [{
id: 1,
message: "destructuring ?",
isLiked: false,
nbOfLike: 0,
image: "http://placeholder.com/random.jpg"
}]
return (
<section>
{tweets.map(tweet => {
return <Tweet
key={tweet.id}
message={tweet.message}
isLiked={tweet.isLiked}
nbOfLike={tweet.nbOfLike}
image={tweet.image}
/>
})}
</section>
)
}
# Tips
const TweetPage = () => {
const tweets = [{
id: 1,
message: "destructuring ?",
isLiked: false,
nbOfLike: 0,
image: "http://placeholder.com/random.jpg"
}]
return (
<section>
{tweets.map(tweet => {
return <Tweet key={tweet.id} {...tweet} />
})}
</section>
)
}
Améliorez votre code.
Afficher les logs de la bataille sur la page.
Maintenant le Pokemon vaincu se voit graphiquement.
Old fashion
class LikeComponent extends React.Component {
render() {
return (
<button type="button">
0 j'aime
</button>
);
}
}
# Class component
class LikeComponent extends React.Component {
render() {
const {nbOfLike} = this.props
return (
<button type="button">
{nbOfLike} j'aime
</button>
);
}
}
# Class component
class LikeComponent extends React.Component {
state = {
nbOfLike: 0
}
constructor() {
super();
this.handleLike = this.handleLike.bind(this);
}
handleLike() {
this.setState({
nbOfLike: this.state.nbOfLike + 1
})
}
render() {
return (
<button type="button" onClick={this.handleLike}>
{this.state.nbOfLike} j'aime
</button>
);
}
}
# Class component
const Tweet = () => {
return (
<ErrorBoundary>
<LikeButton />
</ErrorBoundary>
)
}
const LikeButton = () => {
throw new Error('Not Implemented');
}
# Error boundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
sendErrorToSentry(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Page d'erreur.</h1>;
}
return this.props.children;
}
}
# Error boundary
# Error Boundary
Cela ne gère pas les erreurs dans les évènements ! C'est uniquement pour les erreurs critiques.
Créer un composant ErrorBoundary qui affiche un message d'erreur à l'utilisateur et qui dans le même temps afficher l'erreur dans la console du navigateur.
Pour tester, modifiez un composant pour qu'il renvoi une erreur.
Qu'observez-vous ?
D'un composant
# Lifecycle
- Mount
- Updates
- (Errors)
- Unmount
# Lifecycle
class LikeComponent extends React.Component {
state = {
nbOfLike: 0
}
constructor() {
super();
this.handleLike = this.handleLike.bind(this);
}
handleLike() {
this.setState({
nbOfLike: this.state.nbOfLike + 1
})
}
render() {
return (
<button type="button" onClick={this.handleLike}>
{this.state.nbOfLike} j'aime
</button>
);
}
}
# Lifecycle
class LikeComponent extends React.Component {
state = {
nbOfLike: 0
}
constructor() {
super();
this.handleLike = this.handleLike.bind(this);
}
handleLike() {
this.setState({
nbOfLike: this.state.nbOfLike + 1
})
}
componentDidMount() {
sendDataToAnalytics();
}
render() {
return (
<button type="button" onClick={this.handleLike}>
{this.state.nbOfLike} j'aime
</button>
);
}
}
# Lifecycle
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
# Lifecycle
- getDerivedStateFromProps
- shouldComponentUpdate
- getSnapshotBeforeUpdate
- render
- componentDidUpdate
# Lifecycle
- ComponentWillUnmount
Enfin l'explication !
# Les hooks
Toujours appeler les hooks depuis un composant React.
# Les hooks
Toujours appeler les hooks au plus haut niveau de la fonction de rendu.
(Pas dans une boucle, pas dans un if, etc.)
# Les hooks
- Réutiliser de la logique !
- Donner du sens au lifecycle
const LikeButton = () => {
const [nbOfLike, setNbOfLike] = React.useState(0);
return (
<button type="button">
{nbOfLike} j'aime{nbOfLike > 1 ? 's' : ''}
</button>;
);
}
# Les hooks
const Einstein = () => {
const result = React.useMemo(() => whatIsTheLifeGoal(), []);
return result;
}
# Les hooks
const Einstein = ({
humanName
}) => {
const result = React.useMemo(
() => whatIsTheLifeGoal(humanName),
[humanName]
);
return result;
}
# Les hooks
const Einstein = ({
humanName
}) => {
const result = React.useMemo(() => whatIsTheLifeGoal(humanName));
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('A new render occured');
})
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('Id changed', id);
}, [id])
return result;
}
# Les hooks
const Tweet = ({id}) => {
useEffect(() => {
console.log('Initial id', id);
}, [])
return result;
}
# Les hooks
const Tweet = ({id}) => {
// Affiche l'ID dans les Devtools React
useDebugValue(id);
return result;
}
# Les hooks
const TweetEdit = ({id}) => {
const handleSubmit = useCallback(() => {
fetch(`addTweet?userId=${id}`)
}, [id])
return <TweetForm onSubmit={handleSubmit} />;
}
# Les hooks
const TweetEdit = ({id}) => {
const [inputValue, setInputValue] = useState(initialValue);
const onInput = (e) => {
setInputValue(e.target.value);
}
return <input type="text" onInput={onInput} value={inputValue} />;
}
# Les hooks
const useInput = (initialValue) => {
const [inputValue, setInputValue] = useState(initialValue);
const onInput = (e) => {
setInputValue(e.target.value);
}
return [inputValue, onInput];
}
const TweetEdit = ({id}) => {
const [value, onInput] = useInput()
return <input type="text" onInput={onInput} value={value} />;
}
# Les hooks
# Les hooks
Déplacez la logique de bataille dans un hook custom.
Le Rest
# HTTP
Donc pas de manière officiel pour récupérer des données :/
const Tweet = () => {
const [fetchState, setFetchState] = useState({
error: null,
data: null,
isLoading: true
});
useEffect(() => {
fetch('/people.json')
.then(res => res.json())
.then(data => {
setFetchState({
data,
isLoading: false
})
});
})
.catch(err => {
setFetchState({
error: err,
isLoading: false
})
});
}, [])
if(fetchState.isLoading) {
return 'loading...':
}
if(fetchState.error) {
return <>Refresh page</>
}
return <>{fetchState.data}</>
}
# HTTP
const useQuery = (url) => {
const [fetchState, setFetchState] = useState({
error: null,
data: null,
isLoading: true
});
useEffect(() => {
fetch(url)
.then(res => {
if(!res.ok) {
throw new Error('bouu')
}
return res
})
.then(res => res.json())
.then(data => {
setFetchState({
data,
isLoading: false
})
})
.catch(err => {
setFetchState({
error: err,
isLoading: false
})
});
}, [])
return fetchState;
}
const Tweet = () => {
const {
data,
isLoading,
error
} = useQuery('people.json')
if(isLoading) {
return 'loading...':
}
if(error) {
return <>Refresh page</>
}
return <>{data}</>
}
# HTTP
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
const queryClient = new QueryClient()
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Tweet />
</QueryClientProvider>
)
}
const Tweet = () => {
const { isLoading, error, data } = useQuery({
queryKey: ['peoples'],
queryFn: () => fetch('/peoples.json').then(res => res.json())
});
if (isLoading) {
return 'Loading...';
}
if (error) {
return 'Please refresh the page.';
}
return <>{data}</>
}
# HTTP
# HTTP
npm i @tanstack/react-query
# HTTP
// Des concurents existent comme SWR de vercel
/** npm i swr **/
const { data, error, isLoading } = useSWR('/api/user', fetcher)
npm i -D nock
# HTTP
const scope = nock('https://api.github.com')
.get('/repos/atom/atom/license')
.reply(200, {
license: {
key: 'mit',
name: 'MIT License',
spdx_id: 'MIT',
url: 'https://api.github.com/licenses/mit',
node_id: 'MDc6TGljZW5zZTEz',
},
})
Faites appel à une API Pokémon pour récupérer les stats des Pokémons
# router
# Router
# router
# Router
Client
Page web
Serveur
html + css + js + Data
# Router
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (Json)
// Index.js
import { BrowserRouter, RouterProvider } from "react-router-dom";
import { createRoot } from "react-dom/client";
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "/tweet",
element: <MyTweetPage />,
},
]);
createRoot(document.getElementById("#my-app")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
# Router
import { Link, useParams } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "/tweet",
element: <MyTweetPage />,
}
]);
/* ... */
function HomePage() {
return (
<div className="Home">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/tweet">Mes Tweets</Link>
</nav>
</div>
);
}
# Router
import { Link, useParams } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "/tweet/:id",
element: <TweetDetails />,
},
]);
function HomePage() {
return (
<div className="Home">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/tweet/lastTweet">Mon dernier tweet</Link>
</nav>
</div>
);
}
function TweetDetails() {
const {id} = useParams()
return <article>id du tweet: {id}</article>
}
# Router
import { Link, useParams } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "/tweet/:id",
element: <TweetDetails />,
},
]);
function HomePage() {
return (
<div className="Home">
<h1>Tweeeeeet heure</h1>
<nav>
<Link to="/tweet/lastTweet">Mon dernier tweet</Link>
<Link to="/tweet/lastTweet?display=edit">Editer mon dernier tweet</Link>
</nav>
</div>
);
}
function TweetDetails() {
const {id} = useParams()
let [searchParams, setSearchParams] = useSearchParams();
if(searchParams.display === 'edit') {
return /* ... */;
}
return <article>id du tweet: {id}</article>
}
# Router
import { Routes, Route, Link } from "react-router-dom";
const EditTweet = () => {
const user = useUser();
if(!user) {
return <Navigate to="/" />
}
/** ... **/
}
/** OU **/
const EditTweet = () => {
const navigate = useNavigate();
useEffect(() => {
fetch('user')
.catch(() => {
navigate('/')
})
}, [])
/** ... **/
}
# Router
npm i react-router-dom
# Router
# Router
La version 6 est une mise à jour majeur de la librairie.
Pour aller plus loin :
- Nesting
- Gestion de loader
- Busy Indicator
- Error Handling
- ...
En somme, c'est la Philosophie Remix.
# Forms
Donc c'est très manuel, ou alors faut une autre library.
const useInput = (initialValue) => {
const [inputValue, setInputValue] = useState(initialValue);
const onChange = (e) => {
setInputValue(e.target.value);
}
return [inputValue, onChange];
}
const TweetEdit = ({id}) => {
const [value, onChange] = useInput()
return <input type="text" onChange={onChange} value={value} />;
}
# Les hooks
const TweetEdit = ({id}) => {
const [valueInput, onChangeInput] = useInput('')
const [valueSelect, onChangeSelect] = useInput('false')
const [valueTextArea, onChangeTextArea] = useInput('')
const handleSubmit = (e) => {
e.preventDefault();
// ...
}
return (
<form onSubmit={handleSubmit}>
<label>
Titre :
<input type="text" name="title" onChange={onChangeInput} value={valueInput} />
</label>
<label>
Message :
<textarea name="message" value={valueTextArea} onChange={onChangeTextArea} />
</label>
<label>
NSFW :
<select value={valueSelect} onChange={onChangeSelect}>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</label>
<button type="submit">Envoyer</submit>
</form>
);
}
# Les hooks
# Form
# Form
On définit la valeur par défaut de l'input, après c'est le DOM qui gère.
React-Hook-Form !
import { useState } from "react";
import { useForm } from "react-hook-form";
import Header from "./Header";
export function App() {
const { register, handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
// ...
console.log(data)
})
return (
<form onSubmit={onSubmit}>
<input {...register("firstName")} placeholder="Titre" />
<select {...register("NSFW")}>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
<textarea {...register("message")} placeholder="Un T-8 !" />
<button type="submit" >Envoyer</button>
</form>
);
}
# Form
import { useState } from "react";
import { useForm } from "react-hook-form";
import Header from "./Header";
export function App() {
const { register, handleSubmit } = useForm();
const onSubmit = handleSubmit((data) => {
// ...
console.log(data)
})
return (
<form onSubmit={onSubmit}>
<input {...register("firstName", {required: true})} placeholder="Titre" />
{/* ... */}
<button type="submit" >Envoyer</button>
</form>
);
}
# Form
import { useState } from "react";
import { useForm } from "react-hook-form";
import Header from "./Header";
export function App() {
const { register, handleSubmit, formState } = useForm();
const onSubmit = handleSubmit((data) => {
// ...
console.log(data)
})
return (
<form onSubmit={onSubmit}>
<input {...register("firstName", {required: true})} placeholder="Titre" />
<span>{formState.errors?.firstName.message}</span>
{/* ... */}
<button type="submit" >Envoyer</button>
</form>
);
}
# Form
import { useState } from "react";
import { useForm } from "react-hook-form";
import Header from "./Header";
export function App() {
const { register, handleSubmit, formState } = useForm();
const onSubmit = handleSubmit((data) => {
// ...
console.log(data)
})
return (
<form onSubmit={onSubmit}>
<input
{...register("firstName", {
required: {
value: true,
message: 'Ce champ est requis.'
}
})}
placeholder="Titre"
/>
<span>{formState.errors?.firstName.message}</span>
{/* ... */}
<button type="submit" >Envoyer</button>
</form>
);
}
# Form
# Form
Transformez (ou ajouter) la page de selection de Pokémon en formulaire
Dans le JS ?!
import 'app.css';
export const App = () => {
/*
...
*/
}
# CSS
import 'app.css';
export const App = () => {
/*
...
*/
}
# CSS
Tout se passe au moment du build (Webpack, Vite, ...)
import classes from 'app.module.css';
export const App = () => {
return <h1 className={classes.title}>hello world</h1>
}
# CSS
Les CSS module, le premier CSS in JS :)
import styled from 'styled-component';
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: #BF4F74;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
# CSS
Demo !
Passer des props, ça peut être fastidieux !
// app.tsx
const App = () => {
return (
<MyApp giveIt={'toMe'} />
)
}
// MyApp.tsx
const MyApp = (props) => {
return (
<OtherCmp {...props} />
)
}
// OtherCmp.tsx
const OtherCmp = () => {
return (
<div>{giveIt} gnon</div>
)
}
# Context
// Fichier A
const CookieContext = React.createContext(null);
export const CookieProvider = SessionState.Provider;
export const useCookie = () => React.useContext(CookieContext);
// // app.tsx
const App = () => {
return (
<CookieProvider value={{giveIt: "toMe"}}>
<MyApp />
</CookieProvider>
)
}
// MyApp.tsx
const MyApp = (props) => {
return (
<OtherCmp />
)
}
// OtherCmp.tsx
const OtherCmp = () => {
const {giveIt} = useCookie()
return (
<div>{giveIt} gnon</div>
)
}
# Context
Intéragir avec le DOM
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` fait référence au champ textuel monté dans le DOM
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Donner le focus au champ</button>
</>
);
}
# Les refs
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Total : {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
# Les autres hooks
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
# Les autres hooks
const Tweet = () => {
useLayoutEffect(() => {
// Interact with another framework, ...
}, [])
return result;
}
# Les autres hooks
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
# Les autres hooks
# Les autres hooks
PHP ? C'est inspirant !
# Next.js
Framework JS et le syndrome de la page blanche
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularBdd</title>
<base href="/">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<div id="root">loading...</div>
</body>
</html>
Démo !
# Next.js
Next.JS
React
1 fichier / APP
# Next.js
Client
SPA
Serveur
Statique
Ngnix, GH-page, surge.sh, ...
html + css + js
API
Java, Node, ...
data (JSON)
# Next.js
Client
APP Angular
Serveur
html + css + js
API
Java, Node, ...
more data (JSON)
+ initial data
data (JSON)
Next.js
# Next.js
Next.JS
React
1 fichier HTML / page
API
# Next.js
Client
APP React
Serveur
html + css + js
+ data
Statique
API
Java, Node, ...
more data (JSON)
# Next.js
Client
APP React
Serveur
html + css + js
API
Java, Node, ...
more data (JSON)
+ initial data
data (JSON)
Next.js
Serveur
Static
florent@berthelot.io
github : FBerthelot
@berthel350