"react": "18.2.0"
"typescript": "4.8.4"
let foo = 'bar'
const MAX = 10
const ttc = addTva(100, 20)
let vs. const ? 0-5 let / application
const user = {}
user.name = 'john' // (1)
user = {name: 'lea'} // (2)
const arr = ['a']
arr.push('b') // (3)
arr = ['a', 'b'] // (4)
function addOldSchool(a: number, b: number): number {
return a + b
}
const add = (a:number, b: number): number => {
return a + b
}
const add = (a:number, b: number): number => a + b
const squares = [0, 1, 2, 3, 4].map(x => x * x)
console.log(squares)
const user = {
firstname: 'john',
nickname: 'ninja',
lastname: 'doe',
age: 21
}
const updatedUser = {...user, firstname: 'toto'}
console.log(updatedUser) // ?
const odd = [1, 3, 5, 7, 9]
const even = [2, 4, 6, 8]
const all = [0]
// arr.push([element1[, ...[, elementN]]])
all.push(odd)
all.push(even)
console.log(all) // ?
const odd = [1, 3, 5, 7, 9]
const even = [2, 4, 6, 8]
const all = [0]
// arr.push([element1[, ...[, elementN]]])
for (let i = 0; i < odd.length; i++) {
all.push(odd[i])
}
for (let i = 0; i < even.length; i++) {
all.push(even[i])
}
console.log(all) // ?
const odd: ReadonlyArray<number> = [1, 3, 5, 7, 9]
const even: ReadonlyArray<number> = [2, 4, 6, 8]
const all: ReadonlyArray<number> = [0, ...odd, ...even]
Ne mutez pas pendant la formation, s'il vous plaît.
const defaultValues = [{money: 100}, {level: 0}]
const defaultLevel = defaultValues[1]
const newPlayer = {name: 'player1', data: defaultLevel}
newPlayer.data.level = 2
const newPlayer2 = {name: 'player2', data: defaultLevel}
console.log(newPlayer2) // ?
const defaultValues = [{money: 100}, {level: 0}]
const defaultLevel = defaultValues[1]
const newPlayer = {name: 'player1', data: defaultLevel}
const newPlayerUpdated = {...newPlayer, data: {level : 2}}
const foo = {a: 1, b: 2, c: 'bar'}
const {a} = foo
console.log(a) // ?
const user: User = {
firstname: 'john',
nickname: 'ninja',
lastname: 'doe',
age: 21
}
function getUserName(user: User) {
return `${user.firstname} ${user.lastname}`
}
console.log(getUserName(user))
function getUserName({firstname, lastname}: User) {
return `${firstname} ${lastname}`
}
const getUserName = ({firstname, lastname}: User) =>
`${firstname} ${lastname}`
const user: User = {
firstname: 'john',
lastname: 'doe',
address: {
street: 'avenue des champs élysées',
num: '12'
}
}
function getUserAddress(user: User) {
return `${user.address.num} ${user.address.street}`
}
const getUserAddress = ({address: {num, street}}: User) =>
`${num} ${street}`
const user: User = {
firstname: 'john',
lastname: 'doe',
address: {
street: 'avenue des champs élysées',
num: '12'
}
}
const getUserAddress = ({address: {num: foo, street: bar}}: User) =>
`${foo} ${bar}`
const maths = [x => x + 1, x => x * x, x => x * x * x]
const [plus1, square] = maths
plus1(41) // 42
square(12) // 144
const foo = 42
const bar = 1
const longer = {
foo: foo,
bar: bar
}
const shorter = {
foo,
bar
}
axios.get('/users/42/items')
.then(resp => resp.data)
.then(items => items.map(item => item.id))
.then(ids => {/* use the ids */})
.catch(error => console.log(error.toString()))
const fetchItems = async () => {
try {
const response = await axios.get('/users/42/items')
const items = response.data
const itemIds = items.map(item => item.id))
/* use the ids */
} catch(error){
console.log(error.toString())
}
}
axios.get('/users/42/items')
.then(({data : items}) => items.map(item => item.id))
.then(ids => {/* use the ids */})
.catch(error => console.log(error.toString()))
const fetchItems = async () => {
try {
const {data: items} = await axios.get('/users/42/items')
const itemIds = items.map(item => item.id))
/* use the ids */
} catch(error){
console.log(error.toString())
}
}
const add = function(x) {
return function(y) {
return x + y
}
}
add(2)(3) // ???
const add2 = add(2)
add2(3) // = ?
const add = x => y => x + y // WTF ?!
const add = x =>
y => (x + y)
// easy !
function f() {
this.foo // what is this ?
}
Hello.tsx
interface Props {
name: string
}
const Hello = (props: Props) => Hello {props.name} !
export default Hello
index.tsx
import { createRoot } from 'react-dom/client'
import Hello from "./Hello"
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(<Hello name="world" />)
(props: Props) => (
<li className="contact">
<h2 className="contact-name">{props.name}</h2>
</li>
)
(props: Props) => React.createElement('li', {className: 'contact'},
React.createElement('h2',
{className: 'contact-name'}, props.name)
)
)
const Counter = () => {
let count = 0
return <div onClick={() => count = count + 1}>
Count: {count}
</div>
}
Don't try this at home !
const ComponentWithState = (props) => {
const [count, setCount] = useState<number>(0)
const [user, setUser] = useState<User | null>(null)
const [color, setColor] = useState<string>('green')
return ...
}
Pour mettre un état par défaut, on passe un paramètre à useState()
const [count, setCount] = useState<number>(42)
Mettre à jour un état
const [count, setCount] = useState<number>(0)
setCount(10)
setCount(prevState => prevState + 1)
Récupérer l'état courant
const [count, setCount] = useState<number>(0)
count
const Counter = () => {
const [count, setCount] = useState<number>(0)
return <div onClick={() => setCount(prev => prev + 1)}>
Count: {count}
</div>
}
Props
State
class ButtonApp extends React.Component {
state = {count: 0}
render() {
return <div>
<div>{this.props.title}</div>
<button>{this.props.label}</button>
<div>{this.state.count}</div>
</div>
}
}
const MyComponent = (props: Props) => (
<ul>
{
props.items.map(item =>
<li key={item.id}>{item.label}</li>)
}
</ul>
)
Déclenche un traitement lorsque des données (issues des props ou des states) changent
const UserDetails = ({userId}: Props) => {
const [details, setDetails] = useState<User | null>(null)
useEffect(() => {
axios.get<ReadonlyArray<User>>('/users/' + userId)
.then(({data})) => setDetails(data)
}, [userId])
return <div>...
}
const ShoppingList = ({title}: Props) => {
const [items, setItems] = useState<ReadonlyArray<Item>>([])
// useEffect({...}, [])
return <ShoppingListInternal title={title}
items={items}/>
}
const ShoppingListInternal = ({title, items}: Props) =>
<div>
<h2>{title}</h2>
<ul>{items.map(item =>
<ShoppingItem key={item.id} item={item}/>)}
</ul>
</div>
const ShoppingList = ({title}: Props) => {
const [items, setItems] = useState<ReadonlyArray<User>>([])
// useEffect({...}, [])
const deleteItem = (itemIdToDelete: number) => {
setItems(prev =>
prev.filter(item => item.id !== itemIdToDelete)
)
}
return (<ShoppingListInternal
title={title}
items={items}
del={deleteItem}/>)
}
}
const ShoppingListInternal = ({title, items, del}: Props) => (
<div>
<h2>{title}</h2>
<ul>
{
items.map(item => (<li
onClick={() => del(item.id)}
key={item.id}
>
{item.label}: {item.price}€
</li>))
}
</ul>
</div>)
const ClickableApp = () => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
console.log('click !', e)
}
return <div onClick={handleClick}>Click me !</div>
}
const ClickableApp = () => (
<div
onClick={(e: React.MouseEvent<HTMLDivElement>) =>
console.log('click !', e)}
>
Click me !
</div>
)
Pourquoi ne pas inliner le css dans le js ?
MyComponent = () => {
const mystyle = {backgroundColor: '#F0ABCD'}
return <div style={mystyle}/>
}
Ou utiliser une css globale, une css par composant (css modules avec Webpack)
import './MyComponent.css'
React Components for desktop applications
<Button type="primary" onClick={handleClick}>Go!</Button>
Documentation
React components that implement Google's Material Design
<Button onClick={handleClick}>Go!</Button>
Documentation
Visual primitives for the component age.
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`
<Title>Nice title !</Title>
Documentation
const ComponentWithFragment = () => (
<>
<div>Les</div>
<span>fragments</span>
</>
)
if
{user && {user.login}}
not
{user || No User}
if-else
{user
? {user.login}
: No User}
const MyComponent = () => {
// comment inside JS
return <div>
{/* Comment inside JSX */}
</div>
}
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(
<ButtonApp title="My Application">Press Me! </ButtonApp>
)
export const ButtonApp = ({title, children}: Props) => {
return <div>
<div>{title}</div>
<button>{children}</button>
</div>
}
Le flux est unidirectionel ! (2-way binding)
const userId = window.current_account.id
« Pourquoi utiliser Redux alors qu'on a déjà les variables globales ? »
#TROLL
{type: 'INCREMENT', inc: 3}
{type: 'ADD_TODO', text: 'Buy bread'}
{type: 'SET_USER', user: new User(1, 'toto')}
export const incrementor = (inc: number): IncAction => {
return {type: 'INCREMENT', inc}
}
export const incrementor =
(inc: number): IncAction => ({type: 'INCREMENT', inc})
export const counter = (state: number = 0, action: IncAction) => {
switch (action.type) {
case 'INCREMENT':
return state + action.inc
case 'DECREMENT':
return state - action.inc
default:
return state
}
}
Le state est read-only !
const initialState: State = {user: '', counter: 0}
export const myGlobalReducer =
(state:State = initialState, action: Action): State => {
switch (action.type) {
case 'SET_USER':
return {...state, user: action.user}
case 'INCREMENT':
return {...state, counter: state.counter + 1}
case 'DECREMENT':
return {...state, counter: state.counter - 1}
default:
return state
}
}
Pas vraiment lisible pour des applications moyennes ou grosses...!
export const user =
(state:User|null = null, action: UserAction): User|null => {
switch (action.type) {
case 'SET_USER':
return action.user
default:
return state
}
}
export const counter =
(state:number = 0, action: IncAction): number => {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
import { combineReducers } from 'redux'
export const myGlobalReducer = combineReducers({
user,
counter
})
useSelector
import { useTypedSelector } from './reducers'
export const MyComponent = () => {
const user = useTypedSelector(state => state.user)
return <div>{user.id} / {user.email} </div>
}
useDispatch
import { useDispatch } from 'react-redux'
export const MyComponent = () => {
const dispatch = useDispatch()
return <div onClick={() => dispatch({type: 'INC', inc: 1})}>
Click !
</div>
}
export const SelectPostalCode = () => {
const dispatch = useDispatch()
const currentPc = useTypedSelector(state => state.currentPc)
const postalCodes = useTypedSelector(state => state.postalCodes)
const handleOnChange =
(e: React.ChangeEvent<HTMLSelectElement>) => {
const action = setCurrentPc(e.target.value)
dispatch(action)
}
return (<select onChange={handleOnChange} value={currentPc}>
{
postalCodes.map(pc =>
<option key={pc} value={pc}>{pc}</option> )
}
</select>)
}
Tous les composants "redux" de l'application doivent avoir accès au store.
import { createRoot } from 'react-dom/client'
import {Provider} from 'react-redux'
import { legacy_createStore as createStore} from 'redux'
import {reducer} from './reducers/index'
import {App} from './components/app'
const container = document.getElementById('root')!
const root = createRoot(container)
const store = createStore(reducer)
root.render(<Provider store={store}>
<App/>
</Provider>)
React Query permet de faire des requêtes et de stocker le résultat afin qu'il soit disponible dans toute l'application.
const { data } = useQuery({
queryKey: ['items'],
queryFn: itemsApi.get
})
La clé fonctionne également comme un tableau de dépendances.
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => usersApi.get(userId)
})
useQuery retourne bien plus que les données récupérées.
const { data = {}, refetch, isLoading, isError } = useQuery({
queryKey: ['user', userId],
queryFn: () => usersApi.get(userId)
})
Voir la documentation.
Pour faire des mises à jour, on utilise les mutations.
const { mutateAsync: createUser } = useMutation({
mutationFn: userApi.create,
onSuccess: () => console.log('c est OK !'),
})
<button onClick={() => createUser(myNewUser)}>
CREATE USER
</button>
Voir la documentation.
Il faut initialiser React Query à la racine de l'application
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const container = document.getElementById('root')
const root = createRoot(container)
const queryClient = new QueryClient()
root.render(<QueryClientProvider client={queryClient}>
<MyApp/>
</QueryClientProvider>)
On sait créer des composants et leur passer des données
Créer un Context, et encapsuler l'application à l'interieur du <Context.Provider>
export interface MyContext {
color: string
}
const MyContext = React.createContext<MyContext>({color: 'red'})
export default MyContext
...
return <MyContext.Provider
value={{color: 'blue'}}>
<SomeComponent/>
</MyContext.Provider>
Ensuite on récupère le context là ou l'on veut l'utiliser avec le hook useContext()
import {useContext} from 'react'
import MyContext from './MyContext'
const MyComponent = () => {
const ctx = useContext(MyContext)
return <div style={{backgroundColor: ctx.color}}>
Some text
</div>
}
}
L'objectif et de passer une couleur et un utilisateur d'un composant parent à un composant 'petit-fils'
L'idée est de tester les composants, avec leurs états et leurs props.
L'arrivée des hooks a fait bouger la hiérarchie des outils de test.
import { render } from '@testing-library/react'
describe('MyComponent', () => {
it('should work !', () => {
render(<MyComponent/>)
})
})
import { render } from '@testing-library/react'
it('renders App and its sub components', () => {
const { container } = render(<App />)
expect(container).toMatchSnapshot()
})
it('tests', () => {
expect(1 + 1).toEqual(2)
expect(['a', 'b']).toHaveLength(2)
expect(0 / 2).not.toBeNaN()
})
.toEqual(value), .toBeNull(), .toBeTruthy(), .toHaveLength(number), .not.toBeNaN(), ...
it('should display user infos and winner', () => {
// Given
const { container } = render(<Podium first={player4}
second={player16}
third={player2} />)
const playerInfos = container.querySelector('.infos')
const winner = container.querySelector('#winner')
// Then
expect(playerInfos).toHaveLength(3)
expect(winner.textContent).toMatch('player4')
})
import {render, screen} from '@testing-library/react'
it('renders Alarm', () => {
render(<App/>)
expect(screen.getByText('Menu Alarme')).toBeTruthy()
expect(screen.getAllByTestId('alarme')).toHaveLength(2)
expect(screen.getAllByRole('button')).toHaveLength(2)
})
const mockFunction = jest.fn()
expect(mockFunction).toHaveBeenCalledWith(42)
import {render, fireEvent, screen } from '@testing-library/react'
it('fires events', () => {
render(<NiceInput />)
const input = screen.getByRole('textbox')
fireEvent.change(input, {target: {value: 'new text'}})
})
import { render, screen } from '@testing-library/react'
it('loads data on creation', async () => {
render(<UserInfo id={42} />)
await screen.findByText('Jennifer')
// expect(...)
})
import { render, screen } from '@testing-library/react'
jest.mock('api/users', () => ({
fetchUser: userId =>
Promise.resolve(new User(userId, 'Jennifer')),
}))
describe('UserInfo Component', () => {
it('should display user ', async () => {
render(<UserInfo id={42} />)
await screen.findByText('Jennifer')
// expect...
}
})
L'objectif est d'écrire les tests du TP-03
Il y a différents Routers pour gérer l'historique de la navigation et des URLs:
Pour le BrowserRouter il faut rediriger toutes les requêtes vers index.html, sauf les .css, .js, et les appels aux webservices.
Il faut définir le routeur à la racine de l'application.
import {BrowserRouter as Router,
Routes, Route} from 'react-router-dom'
import { createRoot } from 'react-dom/client'
const container = document.getElementById('root')!
const root = createRoot(container)
root.render(<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/admin/*" element={<Admin />} >
<Route path="users" element={<UsersAdmin />} />
<Route path="articles" element={<ArticlesAdmin />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</Router>)
Plusieurs façons de déclarer les routes :
Il est possible de générer les liens automatiquement (voir la documentation) doc pour l'API
<Link to="/users">Administration des utilisateurs</Link>
Dynamic urls
<Route path="users/*" element={<Users />}>
<Route path=":userId" element={<UserDetail />} />
</Route>
const UserDetail = () => {
const { userId } = useParams()
return <div>
Manage user {userId}
</div>
}
Lorsque l'on définit les sous routes dans les composants (multiple sets of routes), il faut indiquer dans les composants où doivent apparaitrent leurs sous composants. Cela se fait avec <Outlet />
<Route path="/admin/*" element={<Admin />} >
<Route path="users" element={<UsersAdmin />} />
<Route path="articles" element={<ArticlesAdmin />} />
<Route />
const Admin = () => (
Bienvenue sur la page d'administration
<Outlet />
)
L'objectif est de construire une application avec 3 "pages".
Le Virtual DOM est... Une représentation mémoire des composants. Quand le rendu d'un composant est fait à nouveau, le nouveau V-DOM est comparé à l'ancien V-DOM. Si il y a une différence, le DOM du browser est (partiellement) mis à jour.
React.memo permet de memoize un composant React.
const MyComponent = () => ...
export default React.memo(MyComponent)
React.memo permet de memoize un composant React.
const MyComponent = () => ...
export default React.memo(MyComponent, (prevProps, nextProps) => {
return prevProps.user?.id === nextProps.user?.id
})
useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.
const MyComponent = () => (
<Button onClick={e => console.log(e)}>Click me</>
)
useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.
const MyComponent = () => {
const handleClick = (e: SyntheticEvent) => console.log(e)
return <Button onClick={handleClick}>Click me</>
})
useCallback permet de ne pas recréer une nouvelle fonction à chaque exécution (re-render) d'un composant. Ces fonctions sont destinées à être passées comme callback aux composants enfants.
const MyComponent = () => {
const handleClick = useCallback(e => console.log(e), [])
return <Button onClick={handleClick}>Click me</>
})