Nous allons parler de :
- React hook
useCallback - React hook
useMemo - Higher Order Component
memo
Situation type 1 :
Le component reçoit une fonction via props et elle est utilisée dans useEffect.
Situation type 2 :
Je crée un hook qui retourne une fonction qui peut être utilisée dans useEffect.
Situation type 3 :
Le component emballé par React.memo reçoit une fonction via props.
const memoizedDoSomething = useCallback(() => {
doSomething(a, b)
}, [a, b])- prend toujours 2 paramètres
- le premier est une fonction à memoize
- le deuxième est un array de deps
[]: function est memoized une seule fois quand le component mounts[a, b]: nouvelle version est memoized à chaque fois que la changement deaou debest détecté.
Regardons ensemble le code dans ce repo.
// Parent Component
import { useReducer } from "react"
import Planets from "./Planets"
import { reducer } from "../reducers"
const PlanetsApp = () => {
const initialState = {
planets: [],
loading: false,
error: "",
}
const [state, dispatch] = useReducer(reducer, initialState)
const { planets, loading, error } = state
const initFetch = () => dispatch({ type: "FETCH_INIT" })
const fetchPlanets = () => {
fetch(`https://swapi.dev/api/planets/`)
.then(response => {
if (!response.ok) {
throw new Error(
`Nous n'avons pas pu lire le registre des planètes, status : ${response.status}`
)
}
return response.json()
})
.then(data => {
dispatch({ type: "FETCH_SUCCESS", payload: data.results })
})
.catch(error => {
dispatch({ type: "FETCH_FAILURE", payload: error.message })
})
}
return (
<>
<Planets
planets={planets}
initFetch={initFetch}
fetchData={fetchPlanets}
/>
{loading && <p className="text-center">loading...</p>}
{!!error && <p className="alert alert-danger">{error}</p>}
</>
)
}
export default PlanetsApp// Child Component
import { useEffect } from "react"
import Planet from "./Planet"
const Planets = ({ planets, initFetch, fetchData }) => {
useEffect(() => {
initFetch()
fetchData()
}, []) // 👈 ⚠️ warning, nous devons ajouter des deps
return (
<>
<div className="row">
{planets.map(planet => {
return <Planet key={planet.name} planet={planet} />
})}
</div>
</>
)
}
export default Planetsalors :
// Child Component
import { useEffect } from "react"
import Planet from "./Planet"
const Planets = ({ planets, initFetch, fetchData }) => {
useEffect(() => {
initFetch()
fetchData()
}, [initFetch, fetchData]) // no more warnings !!!
return (
<>
<div className="row">
{planets.map(planet => {
return <Planet key={planet.name} planet={planet} />
})}
</div>
</>
)
}
export default PlanetsNo more warnings 🎉 mais ....
https://codepen.io/alyra/pen/WNpXOyP
Voici un schema d'une situation type:
const ParentComponent = () => {
// ici pour la simplicité, state est un nombre
const [state, setState] = useState(0)
const someFunction = () => {
/* qui à l'occasion met à jour state */
setState(s => s + 1)
}
return <ChildComponent someFunction={someFunction} />
}const ChildComponent = ({ someFunction }) => {
React.useEffect(() => {
someFunction()
}, [someFunction])
return <div>Hello !</div>
}ParentComponent mounts (state => 0)
someFunction is created 🆕
ChildCompoment mounts
useEffect runs
state => 1
---
ParentComponent re-renders (state changed)
someFunction is created 🆕
ChildCompoment rerenders
useEffect runs because someFunction 🆕 changed
state => 2
---
ParentComponent re-renders (state changed)
someFunction is created 🆕
ChildCompoment rerenders
useEffect runs because someFunction 🆕 changed
state => 3
---
.... infinite loop 🤯
https://codepen.io/alyra/pen/Rwpjgmy
const ParentComponent = () => {
// ici pour la simplicité, state est un nombre
const [state, setState] = useState(0)
const someFunction = useCallback(() => {
/* qui à l'occasion met à jour state */
setState(s => s + 1)
}, [])
return <ChildComponent someFunction={someFunction} />
}const ChildComponent = ({ someFunction }) => {
React.useEffect(() => {
someFunction()
}, [someFunction])
return <div>Hello !</div>
}ParentComponent mounts (state => 0)
someFunction is created 🆕 and stored ♻ with useCallback
ChildCompoment mounts
useEffect runs
state => 1
---
ParentComponent re-renders (state changed)
someFunction is restored ♻ from useCallback
ChildCompoment rerenders
useEffect does not run because someFunction ♻ didn't change
PlanetsApp mounts (state => {...})
fetchInit is created 🆕
fetchData is created 🆕
Planets mounts
useEffect runs
fetch data calls dispatch
state changes {...}
---
ParentComponent re-renders (state changed)
fetchInit is created 🆕
fetchData is created 🆕
Planets mounts
useEffect runs because fetchInit changed and because fetchData changed
fetch data calls dispatch
state changes {...}
---
ParentComponent re-renders (state changed)
fetchInit is created 🆕
fetchData is created 🆕
Planets mounts
useEffect runs because fetchInit changed and because fetchData changed
fetch data calls dispatch
state changes {...}
---
.... infinite loop 🤯
// Parent Component
import { useReducer, useCallback } from "react"
import Planets from "./Planets"
import { reducer } from "../reducers"
const PlanetsApp = () => {
const initialState = {
planets: [],
loading: false,
error: "",
}
const [state, dispatch] = useReducer(reducer, initialState)
const { planets, loading, error } = state
const initFetch = useCallback(() => dispatch({ type: "FETCH_INIT" }), [])
const fetchPlanets = useCallback(() => {
fetch(`https://swapi.dev/api/planets/`)
/* comme avant */
}, [])
return ( /* comme avant */ )
}
export default PlanetsAppgit checkout pageNous avons ajouté un component <SwitchPage /> et une variable de state page afin de pouvoir visualiser les données de toutes les planètes. Mais ceci ne fonctionne pas correctement.
Comment corriger le code suivant?
import { useReducer, useState, useCallback } from "react"
import Planets from "./Planets"
import { reducer } from "../reducers"
import SwitchPage from "./SwitchPage"
const PlanetsApp = () => {
const initialState = {
planets: [],
loading: false,
error: "",
}
const [page, setPage] = useState(1)
const [state, dispatch] = useReducer(reducer, initialState)
const [active, setActive] = useState(false)
const { planets, loading, error } = state
const initFetch = useCallback(() => dispatch({ type: "FETCH_INIT" }), [])
const fetchPlanets = useCallback(() => {
fetch(`https://swapi.dev/api/planets/?page=${page}`)
.then(response => {
if (!response.ok) {
throw new Error(
`Nous n'avons pas pu lire le registre des planètes, status : ${response.status}`
)
}
return response.json()
})
.then(data => {
dispatch({ type: "FETCH_SUCCESS", payload: data.results })
})
.catch(error => {
dispatch({ type: "FETCH_FAILURE", payload: error.message })
})
}, [])
return (
<>
{!active ? (
<button className="btn btn-dark" onClick={() => setActive(true)}>
Afficher des planètes
</button>
) : (
<>
<SwitchPage page={page} setPage={setPage} />
<Planets
planets={planets}
initFetch={initFetch}
fetchData={fetchPlanets}
/>
</>
)}
{loading && <p className="text-center">loading...</p>}
{!!error && <p className="alert alert-danger">{error}</p>}
</>
)
}
export default PlanetsAppimport { useReducer, useState, useCallback } from "react"
import Planets from "./Planets"
import { reducer } from "../reducers"
import SwitchPage from "./SwitchPage"
const PlanetsApp = () => {
const initialState = {
planets: [],
loading: false,
error: "",
}
const [page, setPage] = useState(1)
const [state, dispatch] = useReducer(reducer, initialState)
const [active, setActive] = useState(false)
const { planets, loading, error } = state
const initFetch = useCallback(() => dispatch({ type: "FETCH_INIT" }), [])
const fetchPlanets = useCallback(() => {
fetch(`https://swapi.dev/api/planets/?page=${page}`)
.then(response => {
if (!response.ok) {
throw new Error(
`Nous n'avons pas pu lire le registre des planètes, status : ${response.status}`
)
}
return response.json()
})
.then(data => {
dispatch({ type: "FETCH_SUCCESS", payload: data.results })
})
.catch(error => {
dispatch({ type: "FETCH_FAILURE", payload: error.message })
})
}, [page]) // 👈
return /* comme avant */
}
export default PlanetsAppRevenons sur notre exemple Todo with API.
Nous allons mettre en place un hook qui permet de controller la fonction dispatch, elle devrait être exécutée uniquement si component est dans la page.
// src/hooks/useSafeDispatch.js
import { useCallback } from "react"
import { useIsMounted } from "./useIsMounted"
export const useSafeDispatch = dispatch => {
const isMounted = useIsMounted()
return useCallback(
action => (isMounted.current ? dispatch(action) : undefined),
[dispatch, isMounted]
)
}
