Built-in
- Mécanique et lifecycle des composants
- Logique MVVM
Third-party
- Manipulation du DOM (react-dom)
- Gestion d'un state global (redux)
- Gestion du réseau (fetch, axios / superagent, react-query)
- Code métier
Model
View
view = f(model)
g(model, event)
View code
Callback code
La vue est une fonction pure du modèle :
La fonction de vue se développe par composition.
const view = App(model)
function App(model: FullAppModel) -> View {
const {sidebarModel, contentModel} = model
return (
<>
{Sidebar(sidebarModel)}
{Content(contentModel)}
</>
)
}
function Sidebar(model: SidebarModel) -> View {
// ...
}
function Content(model: SidebarModel) -> View {
// ...
}
// et ainsi de suite.
Réagir aux événements du DOM
<Button onClick={handleClick} />
async function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
// Pas de return mais des effets ici :
// mettre un state local
setClickCounter(clickCounter => clickCounter + 1)
// déclencher un effet de bord
const result = await submitForm()
setResult(result)
}
Pas une fonction pure, au contraire, un "effet pur".
Dans la vraie vie, on a aussi besoin :
Attacher une variable à mémoriser avec une instance d'un composant :
Pareil, mais en déclenchant un rendu quand on met à jour la valeur :
Pareil, mais avec une logique de reducer pour calculer la prochaine valeur à partir de la valeur actuelle et du trigger :
const clickCounter = useRef<number>(0)
const [clickCounter, setClickCounter] = useState<number>(0)
function reducer(state: number, event: React.MouseEvent) -> number {
switch (event.type) {
case 'DoubleClick':
return state + 2
case 'Click':
return state + 1
default:
return state
}
}
const [clickCounter, dispatch] = useReducer<number>(reducer, 0)
<Button onClick={dispatch} />
Création d'un Contexte :
export const ClickCounterContext = React.createContext<number | undefined>()
Fournir la valeur du contexte dans une partie de l'arborescence des composants :
function ClickCounterPage() {
const [clickCounter, setClickCounter] = useState<number>(0)
return (
<>
<ClickCounterContext.Provider value={clickCounter}>
<SomeOtherComponent />
</ClickCounterContext.Provider>
<Button onClick={() => setClickCounter(clickCounter => clickCounter + 1)} />
</>
)
}
Utilisation type-safe du contexte :
function useClickCounter() -> number {
const clickCounter = useContext(ClickCounterContext)
// fait l'assertion que clickCounter est toujours défini
if (clickCounter === undefined) {
throw new Error('useClickCounter used outside ClickCounterContext.Provider')
}
return clickCounter
}
function SomeOtherComponent() {
const counter = useClickCounter() // number
return <span>Count is {counter}</span>
}
Utilisation basique du contexte :
function SomeOtherComponent() {
const counter = useContext(ClickCounterContext)
if (counter === undefined) {
return null
}
return <span>Count is {counter}</span>
}
Redux :
const ReduxContext = React.createContext<StateShape>(initialState)
function ReduxStore({ children }) {
const [state, dispatch] = useReducer<StateShape>(mainReducer, initialState)
return (
<ReduxContext.Provider value={state}>
{children}
</ReduxContext.Provider>
)
}
function useSelector<T>(selectorFn: (state: StateShape) => T): T {
const fullState = useContext(ReduxContext)
return selectorFn(fullState)
}
function MyApp() {
return (
<ReduxStore>
<Stuff ... />
</ReduxStore>
)
}
Exemple de connexion / déconnexion à un event listener
function MySizeAwareComponent() {
const elt = useRef<HTMLDivElement>()
const [width, setWidth] = useState<number>(0)
useEffect(() => {
if (!elt.current) return
const observer = new ResizeObserver(
(entries) => setWidth(entries[0].contentBoxSize[0])
)
observer.observe(elt.current)
return () => {
if (!elt.current) return
observer.unobserve(elt.current)
}
}, []) // pas de dépendance car setWidth et ref sont stables
return (
<div ref={elt}>
My width is {width}.
</div>
)
}
Exemple de récupérer de la data en fonction d'un ID en prop
function MyDataComponent({ id }) {
const [data, setData] = useState<any>(null)
useEffect(() => {
async function fetchData(id: number) {
const data = await fetchFromBackend(id)
setData(data)
}
fetchData(id)
return () => {
// ici il faudrait annuler la Promise de sorte à ce que
// setData ne soit pas appelée si la Promise est obsolète
}
}, [id])
return (
<pre>
{JSON.stringify(data, null, 2)}
</pre>
)
}
Elles sont comparées via Object.is. Si l'une des dépendances change d'un rendu à l'autre selon Object.is
, alors on lance la fonction de cleanup avec les anciennes valeurs puis la fonction d'effet avec les nouvelles.
useEffect(() => {
async function fetchData() {
const data = await fetchFromBackend(id, params)
setData(data)
}
fetchData()
return () => {
// here we should abort the promise so that setData is not called
// after it finishes if it is outdated
}
}, [id, params, fetchFromBackend])
useEffect(() => {
async function fetchData() {
const data = await fetchFromBackend(id, params)
setData(data)
}
fetchData()
}, [id, params, fetchFromBackend])
id
est un type primitif. Il change d'après Object.is
si et seulement si sa valeur change.params
est un objet. Il change d'après Object.is
quand la référence change, donc dès qu'il est redéfini, même si son contenu n'a pas changé.fetchFromBackend
est une fonction, donc un objet. Elle change d'après Object.is
quand la référence change, donc dès qu'elle est redéfinie, même si son code est identique.async function fetchFromBackend(id) {
return await APIClient.get(`/api/endpoint/${id}`)
}
function MyComponent() {
async function fetchFromBackend(id) {
return await APIClient.get(`/api/endpoint/${id}`)
}
useEffect(() => {
async function fetchData() {
const data = await fetchFromBackend(id)
setData(data)
}
fetchData()
}, [id, fetchFromBackend])
}
Quelle est la différence entre les deux définitions de fetchFromBackend
, en ce qui concerne l'exécution du useEffect
?
Ici, la 2e fetchFromBackend
est instable, nouvelle à chaque rendu, et ce sans raison légitime.
function MyComponent({ endpoint }) {
async function fetchFromBackend(id) {
return await APIClient.get(`/api/${endpoint}/${id}`)
}
useEffect(() => {
fetchFromBackend(id)
}, [id, fetchFromBackend])
}
Ici, fetchFromBackend
a besoin de la variable endpoint
en plus des arguments qu'on lui passe. L'effet et le résultat de cette fonction dépendent, en plus de ses arguments, d'une variable qui existait dans le contexte à sa création, et qu'elle "embarque" avec elle (mécanisme de closure) et utilise lors de l'appel.
Avec la mécanique de closure, pour que l'effet de la fonction change avec le changement de la prop endpoint
, il faut recréer la fonction, car la fonction précédemment créée embarque la valeur précédente de endpoint
.
Donc à chaque fois que endpoint
change, il faut recréer la fonction. Mais pas à chaque rendu !
function MyComponent({ endpoint }) {
const fetchFromBackend = useMemo(() => {
return function(id) {
return APIClient.get(`/api/${endpoint}/${id}`)
}
}, [endpoint]);
// équivalent mais plus court
const fetchFromBackend = useCallback((id) => {
return APIClient.get(`/api/${endpoint}/${id}`)
}, [endpoint]);
useEffect(() => {
fetchFromBackend(id)
}, [id, fetchFromBackend])
}
useMemo
ou useCallback
: utilitaire qui retourne un nouvel objet / fonction lorsque les dépendances changent.
Le useEffect s'exécutera lorsque id
ou endpoint
changent.
Est-ce que c'est ce qu'on veut ?
function MyComponent({ endpoint }) {
const fetchFromBackend = useCallback((id) => {
return APIClient.get(`/api/${endpoint}/${id}`)
}, [endpoint]);
useEffect(() => {
fetchFromBackend(id)
}, [id, fetchFromBackend])
}
Le useEffect
s'exécutera lorsque id
ou endpoint
changent. Est-ce que c'est ce qu'on veut ?
React part du principe que si la conséquence change, il faut réexécuter l'effet.
Si vous naviguez entre deux pages ayant la même structure de composants mais juste l'ID ou l'endpoint qui changent, alors le composant reste instancié, et sa prop change. Vous avez besoin de refetcher.
A vous de bien indiquer à React, de quelles variables dépendent les conséquences de vos effets. Il sait le déterminer tout seul, sauf pour les objets / fonctions créées au rendu, qu'il faut parfois stabiliser avec useMemo
/ useCallback
.
function MyComponent() {
const [data, setData] = useState<DataT>()
useEffect(() => {
async function run(id) {
const data = await APIClient.get(`/api/endpoint/${id}`)
setData(data)
}
run(id)
}, [id])
async function onSubmit(formData) {
const result = await APIClient.put(`/api/endpoint/${id}`, formData)
setData(result)
}
return (
<div>
<DataDisplay data={data} />
<DataForm initialData={data} onSubmit={onSubmit} />
</div>
)
}
Code de vue
Code de callback
Code d'effet
Si vous mettez du code de ➡️ dans du ⬇️ | Vue | Callback | Effet |
---|---|---|---|
Vue | ✅ | Boucle infinie | Boucle infinie |
Callback | Ne fait rien | ✅ | Pas assez d'exécutions |
Effet | Ne fait rien | Trop d'exécution | ✅ |
Hâte de relire vos PR de front ;)