Loupe

Les refs avec React : Comment les utiliser dans les functional components

Bonjour !

Aujourd’hui, nous allons parler des refs dans React. C’est un concept relativement assez connu et utilisé de React qui facilite bien la vie dans certains cas. Nous allons revenir sur ce que sont ses Refs ainsi que l’utilisation basique que l’on en fait, puis nous verrons comment utiliser les refs dans le cas des functional components.

Rappel sur les refs

Comme l’indique la documentation React :

Les refs fournissent un moyen d’accéder aux nœuds du DOM ou éléments React créés dans la méthode de rendu.

C’est plutôt clair. Les refs permettent d’assigner dans une variable un nœud du DOM ou l’instance d’un composant. Nous pouvons les créer comme ceci :

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  render() {
    return <div id="myDiv" ref={this.myRef} />;
  }	
}

Dans cet exemple, nous créons une variable myRef qui sera notre conteneur de refs. Lors du rendu, nous spécifions à React d’assigner le nœud DOM de notre div myDiv à notre conteneur de ref myRef.

Une fois ceci fait, nous pouvons accéder via l’attribut current de notre container de ref à notre nœud DOM ou aux instances de nos composants class.

Par exemple, lorsque nous utilisons des refs pour récupérer un nœud du DOM, on peut set le focus sur un input quand on clique sur un bouton:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();

	this.setFocus = this.setFocus.bind(this);
  }

  setFocus() {
	  this.myRef.current.focus = true;
  }

  render() {
    return (
	    <div>
			<input type="text" ref={this.myRef} />
			<button onClick={this.setFocus}>Click to set focus !</button>
	    </div>
    )
  }	
}

Comme dit précédemment, lorsque l’on utilise les Refs sur un composant enfant de type class, nous avons accès à l’instance de ce composant et nous pouvons ainsi appeler une fonction de celui-ci :

class MyComponent extends React.Component {
  constructor(props) {
	super(props);

	this.myRef = React.createRef();

	this.getInputValue= this.getInputValue.bind(this);
  }

  getInputValue() {
	  console.log(this.myRef.current.getValue());
  }

  render() {
    return (
	    <div>
			<MyInput ref={this.myRef} />
			<button onClick={this.getInputValue}>Click to get value !</button>
	    </div>
    )
  }	
}

class MyInput extends React.Component {
	constructor(props) {
		super(props);
		
		this.state = {
			value: ""
		}
		this.getValue = this.getValue.bind(this);
	}

	getValue() {
		return this.state.value;
	}

	render() {
		return (
			<input name="input" onChange={(e) => this.setState({value: e.target.value})} value={this.state.value}/>
		)
	}
}

Il est important de noter cependant que l’usage des refs pour appeler une fonction d’un composant enfant doit rester exceptionnel. Si les refs apparaissent comme le seul moyen pour résoudre un problème, cela est souvent synonyme de problèmes d’architectures ou de workflow. Cependant, cela peut toujours s’avérer utile et c’est important de savoir que cela existe.

React.forwardRef

Parfois, il est utile pour un composant parent d’avoir la ref d’un composant n’étant pas un enfant direct. React fournit donc avec forwardRef une façon de transmettre une ref à travers un composant enfant :

class ImageWrapper extends React.Component {
    constructor(props) {
		super(props);
	
		this.myRef = React.createRef();
	}

    render() {
	    return (
		    <Wrapper ref={this.myRef} />
	    )
    }
}

const Wrapper = React.forwardRef((props, ref) => {
	return (
		<img alt="img" src="img.jpg" ref={ref} />
	);
});

Dans cet exemple, le composant ImageWrapper peut grâce à forwardRef récupérer la ref du composant img qui se trouve dans le composant Wrapper qui lui est un enfant direct de ImageWrapper.
L’utilisation de forwardRef est très souvent marginale et principalement utilisée pour des wrapper de composant UI comme ce que peuvent faire des composants de design-systems.

Utiliser les refs dans les functional components

Bien, rentrons désormais dans le vif du sujet. Voyons voir ce que nous dit la documentation :

Vous ne pouvez pas utiliser l’attribut ref sur les fonctions
composants** parce qu’elles n’ont pas d’instance.

C’est également plutôt clair. Nous ne pouvons pas récupérer l’instance d’un composant de type fonction tout simplement parce qu’une fonction n’a pas de représentation en mémoire et donc pas d’instance. On pourrait dire ici que nous sommes coincés mais il existe évidemment une solution :)

La solution useImperativeHandle

La solution ici va être d’utiliser un hook natif de React nommé useImperativeHandle. Ce hook nous permet de personnaliser une instance de ref avant de la renvoyer au parent. Plus précisément, il nous permet de nous donner le contrôle sur les valeurs que nous souhaitons retourner au parent ainsi que de nous permettre de remplacer des fonctions natives que l’on pourrait retrouver sur des éléments DOM tel que focus, blur, etc;.

Ainsi, en reprenant l’exemple précédent, si nous souhaitions modifier la fonction focus retournée par la ref :

class ImageWrapper extends React.Component {
    constructor(props) {
		super(props);
	
		this.myRef = React.createRef();
	}

    render() {
	    return (
		    <Wrapper ref={this.myRef} />
	    )
    }
}

const Wrapper = React.forwardRef((props, ref) => {
	const imgRef = React.useRef();

	React.useImperativeHandle(ref, () => ({
		focus: () => {
			console.log("Image focused!");
			imgRef.current.focus();
		}
	}));

	return (
		<img alt="img" src="img.jpg" ref={imgRef } />
	);
});

Dans cet exemple, notre ref retournée au parent ne contiendra que la méthode focus qui est notre méthode modifiée.

Bien, vous voyez peut-être comment on peut utiliser cela pour arriver à nos fins. En effet, useImperativeHandle nous permet de faire exactement ce que l’on veut faire. Il nous permet de récupérer un objet ref et de le personnaliser comme bon nous semble. Et tout cela sans forcément avoir à l’assigner directement à un composant enfant. Ainsi :

const Parent = () => {
	let  childref;
	
	const addRef = React.useCallback((r) => {
		childref = r;
	}, [childref]);

	const print = React.useCallback((r) => {
		childref.print();
	}, [childref]);
	
    return (
	    <>
		    <button onClick={print}>Print !</button>
		    <Child ref={addRef} />
	    </>
    )
}

const Child = React.forwardRef((props, ref) => {

	React.useImperativeHandle(ref, () => ({
		print: () => {
			console.log("Toto");
		}
	}));

	return (
		<></>
	);
});

Et voilà ! Avec ceci vous ne devriez plus avoir de soucis à utiliser les refs dans des functional components.
Je me permets de préciser une dernière fois que c’est un cas d’utilisation très rare et que vous si vous êtes amené à devoir utiliser ce genre de solutions, il peut être judicieux de regarder d’abord s’il n’y a pas d’autre façons d’organiser votre code, vos composants et vos données.

Happy Coding !

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus