Passer des fonctions aux composants
Comment passer un gestionnaire d’événements (par exemple onClick) à un composant ?
On peut passer un gestionnaire d’événements et d’autres fonctions dans les props d’un composant, comme n’importe quelle autre valeur :
<button onClick={this.handleClick}>
Si vous avez besoin d’accéder au composant parent dans le gestionnaire d’événements, vous devrez lier la fonction à l’instance du composant (comme ci-dessous).
Comment lier une fonction à l’instance d’un composant ?
Il y’a plusieurs façons de s’assurer que des fonctions ont accès aux attributs du composant comme this.props
et this.state
, qui dépendent de la syntaxe et de l’outillage éventuel que vous utilisez.
Lier la fonction dans le constructeur (ES2015)
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Cliqué');
}
render() {
return <button onClick={this.handleClick}>Cliquez ici</button>;
}
}
Propriété de classe (proposition stade 3)
class Foo extends Component {
// Remarque : cette syntaxe est expérimentale et n'est pas encore standardisée
handleClick = () => {
console.log('Cliqué');
}
render() {
return <button onClick={this.handleClick}>Cliquez ici</button>;
}
}
Lier dans la méthode render
class Foo extends Component {
handleClick() {
console.log('Cliqué');
}
render() {
return <button onClick={this.handleClick.bind(this)}>Cliquez ici</button>;
}
}
Remarque
Utiliser
Function.prototype.bind
dans la méthoderender
crée une nouvelle fonction à chaque fois que le composant est affiché, ce qui peut impacter négativement les performances (voir plus bas).
Fonction fléchée dans le rendu
class Foo extends Component {
handleClick() {
console.log('Cliqué');
}
render() {
return <button onClick={() => this.handleClick()}>Cliquez ici</button>;
}
}
Remarque
Utiliser une fonction fléchée dans la fonction de rendu crée une nouvelle fonction à chaque fois que le composant est affiché, ce qui peut impacter négativement les optimisations basées sur une comparaison stricte d’identité.
Est-il acceptable d’utiliser une fonction fléchée à l’intérieur de la méthode render
?
C’est généralement acceptable, et c’est souvent la façon la plus facile de passer des arguments à une fonction de rappel.
Si vous avez des problèmes de performances, n’hésitez pas à optimiser !
Pourquoi est-il parfois nécessaire de lier une fonction ?
En JavaScript, ces deux extraits de code ne sont pas équivalents :
obj.method();
var method = obj.method;
method();
Lier les méthodes permet de s’assurer que le deuxième extrait de code fonctionne de la même manière que le premier.
Avec React, vous n’avez généralement besoin de lier que les méthodes que vous passez à d’autres composants. Par exemple, <button onClick={this.handleClick}>
passe this.handleClick
, vous devez donc la lier. Cependant, il n’est pas nécessaire de lier la méthode render
ou les méthodes de cycle de vie : on ne les passe pas à d’autres composants.
Cet article de Yehuda Katz (en anglais) explique en détail ce qu’est la liaison de méthode et comment les fonctions marchent en JavaScript.
Pourquoi ma fonction est appelée à chaque affichage du composant ?
Vérifiez que vous n’appelez pas la fonction en la passant au composant :
render() {
// Erroné : handleClick est appelée au lieu d'être passée par référence !
return <button onClick={this.handleClick()}>Cliquez ici</button>
}
Au lieu de ça, passez la fonction (sans les parenthèses) :
render() {
// Correct : handleClick est passée par référence !
return <button onClick={this.handleClick}>Cliquez ici</button>
}
Comment passer un argument à un gestionnaire d’événements ou une fonction de rappel ?
Vous pouvez utiliser une fonction fléchée pour enrober un gestionnaire d’événements et lui passer des arguments :
<button onClick={() => this.handleClick(id)} />
Le code ci-dessous est équivalent à une utilisation de .bind
:
<button onClick={this.handleClick.bind(this, id)} />
Exemple : passer des arguments en utilisant une fonction fléchée
const A = 65 // Code de caractère ASCII
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(letter) {
this.setState({ justClicked: letter });
}
render() {
return (
<div>
Cliqué : {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} onClick={() => this.handleClick(letter)}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
Exemple : passer des arguments en utilisant des attributs data-*
Une autre approche consiste à utiliser des API DOM pour stocker les données nécessaires aux gestionnaires d’événements. Pensez-y si vous avez besoin d’optimiser un grand nombre d’éléments ou si vous avez des composants basés sur React.PureComponent
.
const A = 65 // Code de caractère ASCII
class Alphabet extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = {
justClicked: null,
letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
};
}
handleClick(e) {
this.setState({
justClicked: e.target.dataset.letter
});
}
render() {
return (
<div>
Cliqué : {this.state.justClicked}
<ul>
{this.state.letters.map(letter =>
<li key={letter} data-letter={letter} onClick={this.handleClick}>
{letter}
</li>
)}
</ul>
</div>
)
}
}
Comment éviter qu’une fonction soit appelée trop tôt ou trop souvent ?
Si vous avez un gestionnaire d’événements comme onClick
ou onScroll
et que vous voulez éviter que la fonction de rappel soit appelée trop fréquemment, vous pouvez limiter sa fréquence d‘exécution. Vous pouvez le faire en utilisant :
- le throttling : limitation de la fréquence de déclenchement (exemple :
_.throttle
) - le debouncing : application des modifications après une période d’inactivité (exemple :
_.debounce
) - le throttling basé sur
requestAnimationFrame
: limitation de la fréquence de déclenchement grâce àrequestAnimationFrame
(exemple :raf-schd
)
Jetez un coup d’œil à cette visualisation pour une comparaison intuitive des fonctions throttle
et debounce
.
Remarque
_.debounce
,_.throttle
etraf-schd
fournissent une méthodecancel
pour annuler l’appel à la fonction de rappel différée. Il est recommandé d’appeler cette méthode danscomponentWillUnmount
ou de s’assurer dans la fonction différée que le composant est toujours monté.
Throttle
Le throttling évite qu’une fonction soit appelée plus d’une fois dans un certain laps de temps. L’exemple ci-dessous limite le gestionnaire d’événements de clic pour éviter qu’il soit appelé plus d’une fois par seconde.
import throttle from 'lodash.throttle';
class LoadMoreButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickThrottled = throttle(this.handleClick, 1000);
}
componentWillUnmount() {
this.handleClickThrottled.cancel();
}
render() {
return <button onClick={this.handleClickThrottled}>Charger la suite</button>;
}
handleClick() {
this.props.loadMore();
}
}
Debounce
Le debouncing garantit qu’une fonction ne sera appelée qu’après qu‘un certain temps a passé depuis le dernier appel à cette fonction : on garantit un intervalle minimal entre deux exécutions. Ça peut être utile quand vous avez un calcul complexe à faire en réponse à un événement susceptible d’être déclenché fréquemment (comme le défilement d’une page ou les frappes au clavier). L’exemple ci-dessous utilise cette méthode sur un champ texte avec un délai de 250 ms.
import debounce from 'lodash.debounce';
class Searchbox extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.emitChangeDebounced = debounce(this.emitChange, 250);
}
componentWillUnmount() {
this.emitChangeDebounced.cancel();
}
render() {
return (
<input
type="text"
onChange={this.handleChange}
placeholder="Recherche..."
defaultValue={this.props.value}
/>
);
}
handleChange(e) {
// React recycle les événements, on a donc besoin de lire la valeur avant le différé.
// On aurait aussi pu appeler `event.persist()` et passer l’événement complet.
// Pour en apprendre davantage, consultez fr.reactjs.org/docs/events.html#event-pooling
this.emitChangeDebounced(e.target.value);
}
emitChange(value) {
this.props.onChange(value);
}
}
Throttling basé sur requestAnimationFrame
requestAnimationFrame
permet de différer une fonction pour qu’elle soit exécutée par le navigateur à un moment optimal pour les performances d’affichage. Une fonction différée avec requestAnimationFrame
sera exécutée à la prochaine passe d’affichage (frame, NdT). Le navigateur fera de son mieux pour qu’il y ait toujours 60 passes par seconde (60 fps). Cependant, si le navigateur n’y arrive pas, il limitera naturellement le nombre de passes par seconde. Par exemple, un appareil pourrait n’être capable d’afficher que 30 fps, vous n’obtiendrez donc que 30 passes par seconde. Utiliser requestAnimationFrame
pour limiter l’exécution est une technique pratique afin d’éviter de faire plus de 60 mises à jour par seconde. Si vous faites 100 mises à jour en une seconde, vous créez une charge de travail supplémentaire pour le navigateur que l’utilisateur ne pourra de toutes façons pas voir.
Remarque
Utiliser cette technique ne capturera que la dernière valeur publiée à chaque passe. Vous pouvez voir un exemple du fonctionnement de cette optimisation sur le
MDN
.
import rafSchedule from 'raf-schd';
class ScrollListener extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
// Crée une nouvelle fonction à planifier
this.scheduleUpdate = rafSchedule(
point => this.props.onScroll(point)
);
}
handleScroll(e) {
// Quand on reçoit un événement de défilement de la page, planifier une mise à jour.
// Si on reçoit plusieurs mises à jour dans la même passe, on ne publiera que la
// dernière valeur.
this.scheduleUpdate({ x: e.clientX, y: e.clientY });
}
componentWillUnmount() {
// Annule toute mise à jour en attente puisqu’on démonte le composant
this.scheduleUpdate.cancel();
}
render() {
return (
<div
style={{ overflow: 'scroll' }}
onScroll={this.handleScroll}
>
<img src="/my-huge-image.jpg" />
</div>
);
}
}
Tester votre limitation de fréquence
Afin de tester que votre code de limitation de fréquence fonctionne correctement, il est utile de pouvoir manipuler le temps. Si vous utilisez jest
, vous pouvez utiliser les fausses horloges (mock timers, NdT) pour accélérer le temps. Si vous utilisez requestAnimationFrame
, vous pouvez utiliser raf-stub
afin de contrôler la progression des passes d’animation.