
Considérez la mise en œuvre de la demande de données à l'API en utilisant le nouvel ami React Hooks et les bons vieux amis Render Prop et HOC (Higher Order Component). Découvrez si un nouvel ami est vraiment meilleur que les deux anciens.
La vie ne s'arrête pas, React évolue pour le mieux. En février 2019, React Hooks est apparu dans React 16.8.0. Maintenant, dans les composants fonctionnels, vous pouvez travailler avec l'état local et effectuer des effets secondaires. Personne ne croyait que c'était possible, mais tout le monde l'a toujours voulu. Si vous n'êtes pas à jour avec les détails, cliquez ici pour plus de détails.
React Hooks permet d'abandonner enfin les modèles tels que HOC et Render Prop. Parce que pendant l'utilisation, un certain nombre de réclamations se sont accumulées contre eux:
Afin de ne pas être infondé, regardons un exemple de la façon dont React Hooks est meilleur (ou peut-être pire) Render Prop. Nous considérerons Render Prop, pas HOC, car dans la mise en œuvre, ils sont très similaires et HOC a plus d'inconvénients. Essayons d'écrire un utilitaire qui traite la demande de données vers l'API. Je suis sûr que beaucoup ont écrit cela dans leur vie des centaines de fois, eh bien, voyons si c'est encore mieux et plus facile.
Pour cela, nous utiliserons la bibliothèque axios populaire. Dans le scénario le plus simple, vous devez traiter les états suivants:
- processus d'acquisition de données (isFetching)
- données reçues avec succès (responseData)
- erreur de réception des données (erreur)
- l'annulation de la demande, si au cours de son exécution les paramètres de la demande ont changé, et un nouveau
- annulation d'une demande si ce composant n'est plus dans le DOM
1. Scénario simple
Nous écrirons l'état par défaut et une fonction (réducteur) qui change d'état en fonction du résultat de la requête: succès / erreur.
Qu'est-ce que le réducteur?Pour référence. Reducer nous est venu de la programmation fonctionnelle, et pour la plupart des développeurs JS de Redux. Il s'agit d'une fonction qui prend un état et une action précédents et renvoie l'état suivant.
const defaultState = { responseData: null, isFetching: true, error: null }; function reducer1(state, action) { switch (action.type) { case "fetched": return { ...state, isFetching: false, responseData: action.payload }; case "error": return { ...state, isFetching: false, error: action.payload }; default: return state; } }
Nous réutilisons cette fonction dans deux approches.
Accessoire de rendu
class RenderProp1 extends React.Component { state = defaultState; axiosSource = null; tryToCancel() { if (this.axiosSource) { this.axiosSource.cancel(); } } dispatch(action) { this.setState(prevState => reducer(prevState, action)); } fetch = () => { this.tryToCancel(); this.axiosSource = axios.CancelToken.source(); axios .get(this.props.url, { cancelToken: this.axiosSource.token }) .then(response => { this.dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { this.dispatch({ type: "error", payload: error }); }); }; componentDidMount() { this.fetch(); } componentDidUpdate(prevProps) { if (prevProps.url !== this.props.url) { this.fetch(); } } componentWillUnmount() { this.tryToCancel(); } render() { return this.props.children(this.state); }
Crochets React
const useRequest1 = url => { const [state, dispatch] = React.useReducer(reducer, defaultState); React.useEffect(() => { const source = axios.CancelToken.source(); axios .get(url, { cancelToken: source.token }) .then(response => { dispatch({ type: "fetched", payload: response.data }); }) .catch(error => { dispatch({ type: "error", payload: error }); }); return source.cancel; }, [url]); return [state]; };
Par url, à partir du composant utilisé, nous obtenons les données - axios.get (). Nous traitons le succès et l'erreur, en changeant d'état par l'envoi (action). Retourne l'état au composant. Et n'oubliez pas d'annuler la demande si l'url change ou si le composant est supprimé du DOM. C'est simple, mais vous pouvez écrire de différentes manières. Nous mettons en évidence les avantages et les inconvénients des deux approches:
React Hooks vous permet d'écrire moins de code, et c'est un fait incontestable. Cela signifie que votre efficacité en tant que développeur augmente. Mais vous devez maîtriser un nouveau paradigme.
Lorsqu'il y a des noms de cycles de vie des composants, tout est très clair. Tout d'abord, nous obtenons les données après que le composant est apparu à l'écran (componentDidMount), puis nous les obtenons à nouveau si props.url a changé et avant cela, nous n'oublions pas d'annuler la demande précédente (componentDidUpdate), si le composant a été supprimé du DOM, puis annuler la demande (componentWillUnmount) .
Mais maintenant, nous provoquons un effet secondaire directement dans le rendu, on nous a appris que ce n'est pas possible. Bien que stop, pas vraiment dans le rendu. Et à l'intérieur de la fonction useEffect, qui effectuera quelque chose de manière asynchrone après chaque rendu, ou plutôt validera et rendra le nouveau DOM.
Mais nous n'avons pas besoin après chaque rendu, mais uniquement lors du premier rendu et en cas de modification de l'URL, que nous indiquons comme deuxième argument à utiliserEffet.
Nouveau paradigmeComprendre le fonctionnement de React Hooks nécessite de prendre conscience de nouvelles choses. Par exemple, la différence entre les phases: commit et render. Dans la phase de rendu, React calcule les modifications à appliquer dans le DOM en les comparant avec le résultat du rendu précédent. Et dans la phase de validation, React applique ces modifications au DOM. C'est dans la phase de validation que les méthodes sont appelées: componentDidMount et componentDidUpdate. Mais ce qui est écrit dans useEffect sera appelé après la validation de manière asynchrone et, par conséquent, ne bloquera pas le rendu DOM si vous décidez soudainement accidentellement de synchroniser beaucoup de choses dans l'effet secondaire.
Conclusion - utilisez useEffect. Ecrire moins et plus en sécurité.
Et une autre grande fonctionnalité: useEffect peut nettoyer après l'effet précédent et après avoir supprimé le composant du DOM. Merci à Rx qui a inspiré l'équipe React pour cette approche.
L'utilisation de notre utilitaire avec React Hooks est également beaucoup plus pratique.
const AvatarRenderProp1 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`}> {state => { if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; }} </RenderProp> );
const AvatarWithHook1 = ({ username }) => { const [state] = useRequest(`https://api.github.com/users/${username}`); if (state.isFetching) { return "Loading"; } if (state.error) { return "Error"; } return <img src={state.responseData.avatar_url} alt="avatar" />; };
L'option React Hooks semble à nouveau plus compacte et plus évidente.
Prop Render Prop:
1) on ne sait pas si la mise en page est ajoutée ou seulement la logique
2) si vous devez traiter l'état de Render Prop dans l'état local ou dans les cycles de vie du composant enfant, vous devrez créer un nouveau composant
Ajoutez une nouvelle fonctionnalité - recevoir des données avec de nouveaux paramètres par action de l'utilisateur. Je voulais, par exemple, un bouton qui obtienne un avatar de votre développeur préféré.
2) Mise à jour des données d'action de l'utilisateur
Ajoutez un bouton qui envoie une demande avec un nouveau nom d'utilisateur. La solution la plus simple consiste à stocker le nom d'utilisateur dans l'état local du composant et à transférer le nouveau nom d'utilisateur de l'état, et non les accessoires comme il l'est maintenant. Mais nous aurons ensuite un copier-coller partout où nous aurons besoin de fonctionnalités similaires. Nous avons donc mis cette fonctionnalité dans notre utilitaire.
Nous allons l'utiliser comme ceci:
const Avatar2 = ({ username }) => { ... <button onClick={() => update("https://api.github.com/users/NewUsername")} > Update avatar for New Username </button> ... };
Écrivons une implémentation. Ci-dessous sont écrits uniquement les changements par rapport à la version originale.
function reducer2(state, action) { switch (action.type) { ... case "update url": return { ...state, isFetching: true, url: action.payload, defaultUrl: action.payload }; case "update url manually": return { ...state, isFetching: true, url: action.payload, defaultUrl: state.defaultUrl }; ... } }
Accessoire de rendu
class RenderProp2 extends React.Component { state = { responseData: null, url: this.props.url, defaultUrl: this.props.url, isFetching: true, error: null }; static getDerivedStateFromProps(props, state) { if (state.defaultUrl !== props.url) { return reducer(state, { type: "update url", payload: props.url }); } return null; } ... componentDidUpdate(prevProps, prevState) { if (prevState.url !== this.state.url) { this.fetch(); } } ... update = url => { this.dispatch({ type: "update url manually", payload: url }); }; render() { return this.props.children(this.state, this.update); } }
Crochets React
const useRequest2 = url => { const [state, dispatch] = React.useReducer(reducer, { url, defaultUrl: url, responseData: null, isFetching: true, error: null }); if (url !== state.defaultUrl) { dispatch({ type: "update url", payload: url }); } React.useEffect(() => { …(fetch data); }, [state.url]); const update = React.useCallback( url => { dispatch({ type: "update url manually", payload: url }); }, [dispatch] ); return [state, update]; };
Si vous avez regardé attentivement le code, vous avez remarqué:
- l'url a commencé à être stockée dans notre utilitaire;
- defaultUrl semble identifier que l'url a été mise à jour via des accessoires. Nous devons surveiller le changement de props.url, sinon une nouvelle demande ne sera pas envoyée;
- ajout de la fonction de mise à jour, que nous renvoyons au composant pour envoyer une nouvelle demande en cliquant sur le bouton.
Notez qu'avec Render Prop, nous avons dû utiliser getDerivedStateFromProps pour mettre à jour l'état local en cas de changement de props.url. Et avec React Hooks aucune nouvelle abstraction, vous pouvez immédiatement appeler la mise à jour de l'état dans le rendu - hourra, camarades, enfin!
La seule complication avec React Hooks était de mémoriser la fonction de mise à jour afin qu'elle ne change pas entre les mises à jour des composants. Lorsque, comme dans Render Prop, la fonction de mise à jour est une méthode de classe.
3) Interrogation de l'API au même intervalle ou Interrogation
Ajoutons une autre fonctionnalité populaire. Parfois, vous devez constamment interroger l'API. Vous ne savez jamais que votre développeur préféré a changé la photo de profil et vous n'êtes pas au courant. Ajoutez le paramètre d'intervalle.
Utilisation:
const AvatarRenderProp3 = ({ username }) => ( <RenderProp url={`https://api.github.com/users/${username}`} pollInterval={1000}> ...
const AvatarWithHook3 = ({ username }) => { const [state, update] = useRequest( `https://api.github.com/users/${username}`, 1000 ); ...
Réalisation:
function reducer3(state, action) { switch (action.type) { ... case "poll": return { ...state, requestId: state.requestId + 1, isFetching: true }; ... } }
Accessoire de rendu
class RenderProp3 extends React.Component { state = { ... requestId: 1, } ... timeoutId = null; ... tryToClearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); } } poll = () => { this.tryToClearTimeout(); this.timeoutId = setTimeout(() => { this.dispatch({ type: 'poll' }); }, this.props.pollInterval); }; ... componentDidUpdate(prevProps, prevState) { ... if (this.props.pollInterval) { if ( prevState.isFetching !== this.state.isFetching && !this.state.isFetching ) { this.poll(); } if (prevState.requestId !== this.state.requestId) { this.fetch(); } } } componentWillUnmount() { ... this.tryToClearTimeout(); } ...
Crochets React
const useRequest3 = (url, pollInterval) => { const [state, dispatch] = React.useReducer(reducer, { ... requestId: 1, }); React.useEffect(() => { …(fetch data) }, [state.url, state.requestId]); React.useEffect(() => { if (!pollInterval || state.isFetching) return; const timeoutId = setTimeout(() => { dispatch({ type: "poll" }); }, pollInterval); return () => { clearTimeout(timeoutId); }; }, [pollInterval, state.isFetching]); ... }
Un nouvel accessoire est apparu - pollInterval. À la fin de la demande précédente via setTimeout, nous incrémentons requestId. Avec les hooks, nous avons un autre useEffect, dans lequel nous appelons setTimeout. Et notre ancien useEffect, qui envoie une demande, a commencé à surveiller une autre variable - requestId, qui nous dit que setTimeout a fonctionné, et il est temps d'envoyer la demande d'un nouvel avatar.
Dans Render Prop, j'ai dû écrire:
- comparaison des valeurs requestId et isFetching précédentes et nouvelles
- clear timeoutId à deux endroits
- ajoutez la propriété timeoutId à la classe
React Hooks vous permet d'écrire brièvement et clairement ce que nous avons utilisé pour décrire plus en détail et n'est pas toujours clair.
4) Et ensuite?
Nous pouvons continuer à étendre les fonctionnalités de notre utilitaire: accepter différentes configurations de paramètres de requête, mettre en cache les données, convertir une réponse et des erreurs, mettre à jour de force les données avec les mêmes paramètres - opérations de routine dans toute grande application Web. Sur notre projet, nous avons depuis longtemps intégré ceci dans un composant séparé (attention!). Oui, car il s'agissait d'un accessoire de rendu. Mais avec la sortie de Hooks, nous avons réécrit la fonction (useAxiosRequest) et même trouvé quelques bugs dans l'ancienne implémentation. Vous pouvez voir et essayer ici .