React: Comment devrions-nous configurer des applications pour HMR maintenant que Fast Refresh remplace react-hot-loader?

Créé le 29 août 2019  ·  85Commentaires  ·  Source: facebook/react

Dan Abramov a mentionné que Devtools v4 rendrait react-hot-loader obsolète: https://twitter.com/dan_abramov/status/1144715740983046144?s=20

Moi:
J'ai ce crochet:
require("react-reconciler")(hostConfig).injectIntoDevTools(opts);
Mais HMR a toujours fonctionné sans cela. Est-ce maintenant une nouvelle exigence?

Dan:
Oui, c'est ce que le nouveau mécanisme utilise. Le nouveau mécanisme n'a pas besoin de "react-hot-loader" donc au moment de la mise à jour, vous voudrez supprimer ce paquet. (C'est assez invasif)

Cependant, je ne vois aucune mention de HMR dans la documentation Devtools; maintenant que react-hot-loader est devenu obsolète (et avec elle, la méthode require("react-hot-loader/root").hot ), comment devrions-nous configurer des applications pour HMR dans:

  • Réagir les applications DOM
  • Réagir les applications natives
  • Réagir les applications de rendu personnalisées

Je serais particulièrement intéressé par un guide de migration spécifiquement pour tous ceux qui ont déjà configuré HMR via react-hot-loader .

De plus, pour HMR, est-ce important que nous utilisions les Devtools autonomes ou l'extension de navigateur Devtools?

Question

Commentaire le plus utile

D'accord, voilà.

Qu'est-ce que Fast Refresh?

C'est une réimplémentation du "rechargement à chaud" avec le support complet de React. Il est à l'origine livré pour React Native, mais la plupart de l'implémentation est indépendante de la plate-forme. Le plan est de l'utiliser dans tous les domaines - en remplacement des solutions purement utilisateur (comme react-hot-loader ).

Puis-je utiliser Fast Refresh sur le Web?

Théoriquement, oui, c'est le plan. En pratique, quelqu'un a besoin de l'intégrer aux bundleurs courants sur le Web (par exemple Webpack, Parcel). Je n'ai pas encore réussi à faire ça. Peut-être que quelqu'un veut le récupérer. Ce commentaire est un guide approximatif de la façon dont vous le feriez.

En quoi cela consiste?

Fast Refresh repose sur plusieurs éléments travaillant ensemble:

  • Mécanisme de "remplacement de module à chaud" dans le système de modules.

    • Cela est généralement également fourni par le bundler.

    • Par exemple, dans webpack, module.hot API vous permet de le faire.

  • Rendu React 16.9.0+ (par exemple React DOM 16.9)
  • react-refresh/runtime point d'entrée
  • react-refresh/babel Plugin Babel

Vous voudrez probablement travailler sur la partie intégration. Ie intégrant react-refresh/runtime avec le mécanisme de "remplacement de module à chaud" de Webpack.

À quoi ressemble l'intégration?

⚠️⚠️⚠️ POUR ÊTRE CLAIR, CECI EST UN GUIDE POUR LES PERSONNES QUI SOUHAITENT METTRE EN ŒUVRE L'INTÉGRATION ELLES. PROCÉDEZ À VOTRE PROPRE ATTENTION!

Il y a quelques choses que vous voulez faire au minimum:

  • Activez HMR dans votre bundler (par exemple, webpack)
  • Assurez-vous que React est 16.9.0+
  • Ajoutez react-refresh/babel à vos plugins Babel

À ce stade, votre application devrait planter. Il doit contenir des appels aux fonctions $RefreshReg$ et $RefreshSig$ qui ne sont pas définies.

Ensuite, vous devez créer un nouveau point d'entrée JS qui doit s'exécuter avant tout code dans votre application , y compris react-dom (!) Ceci est important; s'il s'exécute après react-dom , rien ne fonctionnera. Ce point d'entrée devrait faire quelque chose comme ceci:

if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
  const runtime = require('react-refresh/runtime');
  runtime.injectIntoGlobalHook(window);
  window.$RefreshReg$ = () => {};
  window.$RefreshSig$ = () => type => type;
}

Cela devrait corriger les plantages. Mais cela ne fera toujours rien parce que ces implémentations de $RefreshReg$ et $RefreshSig$ sont noops. Les connecter est la viande du travail d'intégration que vous devez faire.

La façon dont vous faites cela dépend de votre bundler. Je suppose qu'avec webpack, vous pouvez écrire un chargeur qui ajoute du code avant et après l'exécution de chaque module. Ou peut-être qu'il y a un crochet pour injecter quelque chose dans le modèle de module. Quoi qu'il en soit, ce que vous voulez réaliser, c'est que chaque module ressemble à ceci:

// BEFORE EVERY MODULE EXECUTES

var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) => {
  // Note module.id is webpack-specific, this may vary in other bundlers
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {

  // !!!
  // ...ACTUAL MODULE SOURCE CODE...
  // !!!

} finally {
  window.$RefreshReg$ = prevRefreshReg;
  window.$RefreshSig$ = prevRefreshSig;
}

L'idée ici est que notre plugin Babel émet des appels à ces fonctions, puis notre intégration ci-dessus lie ces appels à l'ID du module. Pour que le runtime reçoive des chaînes comme "path/to/Button.js Button" lorsqu'un composant est en cours d'enregistrement. (Ou, dans le cas de webpack, les identifiants seraient des nombres.) N'oubliez pas que la transformation Babel et ce wrapping ne doivent se produire qu'en mode développement.

Au lieu d'encapsuler le code du module, il existe peut-être un moyen d'ajouter un essai / enfin comme celui-ci autour de l'endroit où le bundler initialise réellement la fabrique de modules. Comme nous le faisons ici dans Metro (RN bundler). Ce serait probablement mieux parce que nous n'aurions pas besoin de gonfler chaque module, ou de nous soucier d'introduire une syntaxe illégale, par exemple lors de l'encapsulation de import avec in try / finally .

Une fois que vous avez connecté cela, vous avez un dernier problème. Votre bundler ne sait pas que vous gérez les mises à jour, il recharge donc probablement la page de toute façon. Vous devez lui dire de ne pas le faire. C'est encore une fois spécifique au bundler, mais l'approche que je suggère est de vérifier si toutes les exportations sont des composants React , et dans ce cas, "accepter" la mise à jour. Dans Webpack, cela pourrait ressembler à quelque chose:


// ...ALL MODULE CODE...

const myExports = module.exports; 
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack

if (isReactRefreshBoundary(myExports)) {
  module.hot.accept(); // Depends on your bundler
  enqueueUpdate();
}

Qu'est-ce que isReactRefreshBoundary ? C'est une chose qui énumère les exportations de manière superficielle et détermine si elle n'exporte que des composants React. C'est ainsi que vous décidez d'accepter ou non une mise à jour. Je ne l'ai pas copié-collé ici mais cette implémentation pourrait être un bon début. (Dans ce code, Refresh fait référence à react-refresh/runtime export).

Vous voudrez également enregistrer manuellement toutes les exportations car la transformation Babel n'appellera que $RefreshReg$ pour les fonctions. Si vous ne le faites pas, les mises à jour des classes ne seront pas détectées.

Enfin, la fonction enqueueUpdate() serait quelque chose de partagé entre les modules qui débounce et effectue la mise à jour réelle de React.

const runtime = require('react-refresh/runtime');

let enqueueUpdate = debounce(runtime.performReactRefresh, 30);

À ce stade, vous devriez avoir quelque chose qui fonctionne.

Nuances

Il y a des attentes d'expérience de base qui me tiennent à cœur et qui vont dans la stratégie de marque «Fast Refresh». Il devrait être en mesure de récupérer correctement une erreur de syntaxe, une erreur d'initialisation de module ou une erreur de rendu. Je n'entrerai pas dans ces mécanismes en détail, mais je suis convaincu que vous ne devriez pas appeler votre expérience "Fast Refresh" tant qu'elle n'a pas bien géré ces cas.

Malheureusement, je ne sais pas si webpack peut prendre en charge tout cela, mais nous pouvons demander de l'aide si vous arrivez à un état de fonctionnement quelque peu mais que vous restez bloqué. Par exemple, j'ai remarqué que l'API accept() webpack rend la récupération d'erreur plus difficile (vous devez accepter une version _précédente_ du module après une erreur), mais il existe un moyen de contourner cela. Une autre chose sur laquelle nous devrons revenir est d '"enregistrer" automatiquement toutes les exportations, et pas seulement celles trouvées par le plugin Babel. Pour l'instant, ignorons cela, mais si vous avez quelque chose qui fonctionne par exemple pour Webpack, je peux envisager de le polir.

De même, nous aurions besoin de l'intégrer avec une expérience de "boîte d'erreur", similaire à react-error-overlay nous avons dans Create React App. Cela a une certaine nuance, comme la boîte d'erreur devrait disparaître lorsque vous corrigez une erreur. Cela demande également un travail supplémentaire que nous pouvons faire une fois la fondation en place.

Faites moi savoir si vous avez des questions!

Tous les 85 commentaires

Il y a une certaine confusion. Le nouveau DevTools n'active pas le rechargement à chaud (ou n'a rien à voir avec le rechargement). Au contraire, les changements de rechargement à chaud sur lesquels Dan a travaillé utilisent le «hook» que DevTools et React utilisent pour communiquer. Il s'ajoute au milieu pour pouvoir recharger.

J'ai modifié le titre pour supprimer la mention de DevTools (car cela peut prêter à confusion).

Quant à la question de savoir comment utiliser le nouveau HMR, je ne pense pas connaître les dernières réflexions à ce sujet. Je vois que @gaearon a un PR wip sur le repo CRA:
https://github.com/facebook/create-react-app/pull/5958

Quant à la question de savoir comment utiliser le nouveau HMR, je ne pense pas connaître les dernières réflexions à ce sujet. Je vois que @gaearon a un PR wip sur le repo CRA:

Pour clarifier pour les lecteurs, que PR est très dépassé et n'est plus pertinent.


J'ai besoin d'écrire quelque chose sur le fonctionnement de Fast Refresh et comment l'intégrer. Je n'ai pas encore eu le temps.

D'accord, voilà.

Qu'est-ce que Fast Refresh?

C'est une réimplémentation du "rechargement à chaud" avec le support complet de React. Il est à l'origine livré pour React Native, mais la plupart de l'implémentation est indépendante de la plate-forme. Le plan est de l'utiliser dans tous les domaines - en remplacement des solutions purement utilisateur (comme react-hot-loader ).

Puis-je utiliser Fast Refresh sur le Web?

Théoriquement, oui, c'est le plan. En pratique, quelqu'un a besoin de l'intégrer aux bundleurs courants sur le Web (par exemple Webpack, Parcel). Je n'ai pas encore réussi à faire ça. Peut-être que quelqu'un veut le récupérer. Ce commentaire est un guide approximatif de la façon dont vous le feriez.

En quoi cela consiste?

Fast Refresh repose sur plusieurs éléments travaillant ensemble:

  • Mécanisme de "remplacement de module à chaud" dans le système de modules.

    • Cela est généralement également fourni par le bundler.

    • Par exemple, dans webpack, module.hot API vous permet de le faire.

  • Rendu React 16.9.0+ (par exemple React DOM 16.9)
  • react-refresh/runtime point d'entrée
  • react-refresh/babel Plugin Babel

Vous voudrez probablement travailler sur la partie intégration. Ie intégrant react-refresh/runtime avec le mécanisme de "remplacement de module à chaud" de Webpack.

À quoi ressemble l'intégration?

⚠️⚠️⚠️ POUR ÊTRE CLAIR, CECI EST UN GUIDE POUR LES PERSONNES QUI SOUHAITENT METTRE EN ŒUVRE L'INTÉGRATION ELLES. PROCÉDEZ À VOTRE PROPRE ATTENTION!

Il y a quelques choses que vous voulez faire au minimum:

  • Activez HMR dans votre bundler (par exemple, webpack)
  • Assurez-vous que React est 16.9.0+
  • Ajoutez react-refresh/babel à vos plugins Babel

À ce stade, votre application devrait planter. Il doit contenir des appels aux fonctions $RefreshReg$ et $RefreshSig$ qui ne sont pas définies.

Ensuite, vous devez créer un nouveau point d'entrée JS qui doit s'exécuter avant tout code dans votre application , y compris react-dom (!) Ceci est important; s'il s'exécute après react-dom , rien ne fonctionnera. Ce point d'entrée devrait faire quelque chose comme ceci:

if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
  const runtime = require('react-refresh/runtime');
  runtime.injectIntoGlobalHook(window);
  window.$RefreshReg$ = () => {};
  window.$RefreshSig$ = () => type => type;
}

Cela devrait corriger les plantages. Mais cela ne fera toujours rien parce que ces implémentations de $RefreshReg$ et $RefreshSig$ sont noops. Les connecter est la viande du travail d'intégration que vous devez faire.

La façon dont vous faites cela dépend de votre bundler. Je suppose qu'avec webpack, vous pouvez écrire un chargeur qui ajoute du code avant et après l'exécution de chaque module. Ou peut-être qu'il y a un crochet pour injecter quelque chose dans le modèle de module. Quoi qu'il en soit, ce que vous voulez réaliser, c'est que chaque module ressemble à ceci:

// BEFORE EVERY MODULE EXECUTES

var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) => {
  // Note module.id is webpack-specific, this may vary in other bundlers
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {

  // !!!
  // ...ACTUAL MODULE SOURCE CODE...
  // !!!

} finally {
  window.$RefreshReg$ = prevRefreshReg;
  window.$RefreshSig$ = prevRefreshSig;
}

L'idée ici est que notre plugin Babel émet des appels à ces fonctions, puis notre intégration ci-dessus lie ces appels à l'ID du module. Pour que le runtime reçoive des chaînes comme "path/to/Button.js Button" lorsqu'un composant est en cours d'enregistrement. (Ou, dans le cas de webpack, les identifiants seraient des nombres.) N'oubliez pas que la transformation Babel et ce wrapping ne doivent se produire qu'en mode développement.

Au lieu d'encapsuler le code du module, il existe peut-être un moyen d'ajouter un essai / enfin comme celui-ci autour de l'endroit où le bundler initialise réellement la fabrique de modules. Comme nous le faisons ici dans Metro (RN bundler). Ce serait probablement mieux parce que nous n'aurions pas besoin de gonfler chaque module, ou de nous soucier d'introduire une syntaxe illégale, par exemple lors de l'encapsulation de import avec in try / finally .

Une fois que vous avez connecté cela, vous avez un dernier problème. Votre bundler ne sait pas que vous gérez les mises à jour, il recharge donc probablement la page de toute façon. Vous devez lui dire de ne pas le faire. C'est encore une fois spécifique au bundler, mais l'approche que je suggère est de vérifier si toutes les exportations sont des composants React , et dans ce cas, "accepter" la mise à jour. Dans Webpack, cela pourrait ressembler à quelque chose:


// ...ALL MODULE CODE...

const myExports = module.exports; 
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack

if (isReactRefreshBoundary(myExports)) {
  module.hot.accept(); // Depends on your bundler
  enqueueUpdate();
}

Qu'est-ce que isReactRefreshBoundary ? C'est une chose qui énumère les exportations de manière superficielle et détermine si elle n'exporte que des composants React. C'est ainsi que vous décidez d'accepter ou non une mise à jour. Je ne l'ai pas copié-collé ici mais cette implémentation pourrait être un bon début. (Dans ce code, Refresh fait référence à react-refresh/runtime export).

Vous voudrez également enregistrer manuellement toutes les exportations car la transformation Babel n'appellera que $RefreshReg$ pour les fonctions. Si vous ne le faites pas, les mises à jour des classes ne seront pas détectées.

Enfin, la fonction enqueueUpdate() serait quelque chose de partagé entre les modules qui débounce et effectue la mise à jour réelle de React.

const runtime = require('react-refresh/runtime');

let enqueueUpdate = debounce(runtime.performReactRefresh, 30);

À ce stade, vous devriez avoir quelque chose qui fonctionne.

Nuances

Il y a des attentes d'expérience de base qui me tiennent à cœur et qui vont dans la stratégie de marque «Fast Refresh». Il devrait être en mesure de récupérer correctement une erreur de syntaxe, une erreur d'initialisation de module ou une erreur de rendu. Je n'entrerai pas dans ces mécanismes en détail, mais je suis convaincu que vous ne devriez pas appeler votre expérience "Fast Refresh" tant qu'elle n'a pas bien géré ces cas.

Malheureusement, je ne sais pas si webpack peut prendre en charge tout cela, mais nous pouvons demander de l'aide si vous arrivez à un état de fonctionnement quelque peu mais que vous restez bloqué. Par exemple, j'ai remarqué que l'API accept() webpack rend la récupération d'erreur plus difficile (vous devez accepter une version _précédente_ du module après une erreur), mais il existe un moyen de contourner cela. Une autre chose sur laquelle nous devrons revenir est d '"enregistrer" automatiquement toutes les exportations, et pas seulement celles trouvées par le plugin Babel. Pour l'instant, ignorons cela, mais si vous avez quelque chose qui fonctionne par exemple pour Webpack, je peux envisager de le polir.

De même, nous aurions besoin de l'intégrer avec une expérience de "boîte d'erreur", similaire à react-error-overlay nous avons dans Create React App. Cela a une certaine nuance, comme la boîte d'erreur devrait disparaître lorsque vous corrigez une erreur. Cela demande également un travail supplémentaire que nous pouvons faire une fois la fondation en place.

Faites moi savoir si vous avez des questions!

Les erreurs de syntaxe / les erreurs d'initialisation devraient être «assez faciles» à gérer avant de dire à React de démarrer un rendu, mais comment les erreurs de rendu interagiraient-elles avec les limites d'erreur?

Si une erreur de rendu se produit, cela déclenchera la limite d'erreur la plus proche qui se snapshotera elle-même dans un état d'erreur, et il n'y a pas de moyen générique de dire aux limites d'erreur que leurs enfants sont fixés par magie après une actualisation en direct. Chaque composant actualisable doit-il / doit-il obtenir gratuitement sa propre limite d'erreur ou la gestion des erreurs fonctionne-t-elle différemment dans le réconciliateur lorsque la prise en charge de l'exécution est détectée lors de l'initialisation?

toutes les exportations sont des composants React, et dans ce cas, "accepter" la mise à jour

Existe-t-il un moyen de détecter ces composants? Autant que je sache - non. Sauf export.toString().indexOf('React')>0 , mais cela cesserait de fonctionner avec n'importe quel HOC appliqué.
De plus, «accepter soi-même» à la fin du fichier n'est pas sujet aux erreurs - le nouveau handle d'acceptation ne serait pas établi et la prochaine mise à jour ferait remonter à la limite supérieure, c'est pourquoi require("react-hot-loader/root").hot été créé.

Dans tous les cas - il semble que si l'on jetait tout le code spécifique à la réaction à partir de react-hot-loader , en gardant l'API externe intacte - ce serait suffisant et applicable à toutes les installations existantes.

Utiliser react-refresh/babel 0.4.0 me donne cette erreur sur un grand nombre de fichiers:

ERROR in ../orbit-app/src/hooks/useStores.ts
Module build failed (from ../node_modules/babel-loader/lib/index.js):
TypeError: Cannot read property '0' of undefined
    at Function.get (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/index.js:115:33)
    at NodePath.unshiftContainer (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/modification.js:191:31)
    at PluginPass.exit (/Users/nw/projects/motion/orbit/node_modules/react-refresh/cjs/react-refresh-babel.development.js:546:28)

J'ai réduit ce fichier à la chose la plus simple qui le cause:

import { useContext } from 'react'

export default () => useContext()

Si une erreur de rendu se produit, cela déclenchera la limite d'erreur la plus proche qui se snapshotera dans un état d'erreur, et il n'y a pas de moyen générique de dire aux limites d'erreur que leurs enfants sont peut-être fixés par magie après une actualisation en direct.

Le code de rafraîchissement rapide dans React se souvient des limites actuellement en échec. Chaque fois qu'une mise à jour Fast Refresh est planifiée, elle les remontera toujours.

S'il n'y a pas de limites, mais qu'une racine a échoué lors de la mise à jour, Fast Refresh tentera de restituer cette racine avec son dernier élément.

Si la racine a échoué lors du montage, runtime.hasUnrecoverableErrors() vous le dira. Ensuite, vous devez forcer un rechargement. Nous pourrions gérer ce cas plus tard, je n'ai pas encore eu le temps de le résoudre.

L'utilisation de react-refresh / babel 0.4.0 me donne cette erreur sur un grand nombre de fichiers:

Déposer un nouveau problème pls?

Existe-t-il un moyen de détecter ces composants?

J'ai lié à mon implémentation, qui elle-même utilise Runtime.isLikelyAReactComponent() . Ce n'est pas parfait mais c'est assez bon.

la nouvelle poignée d'acceptation ne serait pas établie et la prochaine mise à jour ferait remonter la limite supérieure

Pouvez-vous faire un exemple? Je ne suis pas. Quoi qu'il en soit, c'est quelque chose de spécifique au bundler. J'ai fait faire à Metro ce que je voulais. Nous pouvons demander à webpack d'ajouter quelque chose s'il nous manque une API.

Le but ici est de ré-exécuter le moins de modules possible tout en garantissant la cohérence. Nous ne voulons pas diffuser de mises à jour à la racine pour la plupart des modifications.

il semble que si l'on jetait tout le code spécifique à react-hot-loader, en gardant l'API externe intacte

Peut-être, même si j'aimerais également supprimer le conteneur de niveau supérieur. Je souhaite également une intégration plus étroite avec la boîte d'erreur. Peut-être que cela peut encore être appelé react-hot-loader .

Au fait, j'ai édité mon guide pour inclure une pièce manquante que j'avais oubliée - l'appel performReactRefresh . C'est ce qui planifie réellement les mises à jour.

isLikelyComponentType(type) {
   return typeof type === 'function' && /^[A-Z]/.test(type.name);
},

Je ne me sentirais pas en sécurité avec une telle logique. Même si toutes les CapitalizedFunctions sont presque toujours des composants React - de nombreux modules (du mien) ont également d'autres exportations. Par exemple _exports-for-tests_. Ce n'est pas un problème, mais crée une certaine _imprévisibilité_ - une frontière dynamique pourrait être créée à tout moment ... ou ne pas être créée après un changement de ligne.
Ce qui pourrait casser le test isLikelyComponentType :

  • exporté mapStateToProps (pour les tests, non utilisé dans un code de production)
  • exporté hook (et c'est ok)
  • exporté Class qui pourrait ne pas être une classe de réaction (ne le ferait pas, mais devrait)

Donc - il y aurait des cas où une frontière chaude serait établie, mais pas, et il y aurait des cas où une frontière chaude serait établie, mais ne le sera pas. Cela ressemble à un bon vieux rechargement à chaud instable que nous n'aimons pas tous les deux :)

Il y a un endroit où l'application de la frontière chaude ne serait pas si imprévisible, et serait tout à fait attendue - une frontière thing ou domain , ou un index de répertoire, c'est-à-dire une réexportation index.js une "API publique" du Component.js dans le même répertoire (pas un afaik de style Facebook).

En d'autres termes, tout comme vous l'avez fait dans Metro, mais avec plus de limitations appliquées. Tout le reste, comme la règle de linting pour établir une telle limite pour tout composant chargé paresseusement, pourrait être utilisé pour appliquer le comportement correct.

En parlant de cela - un rafraîchissement rapide à chaud gérerait Lazy? Est-ce qu'il devrait avoir une limite de l'autre côté du import ?

J'ai essayé rapidement de voir la magie dans le navigateur et c'est tellement agréable :) J'ai fait la chose la plus simple possible, c'est-à-dire coder en dur tout le code d'instrumentation, donc pas encore de plugins webpack

Kapture 2019-09-07 at 23 09 04

Repo ici: https://github.com/pekala/react-refresh-test

Juste curieux mais pour webpack, ne pourriez-vous pas simplement avoir un plugin babel pour envelopper l'essayage / enfin? Je veux juste être sûr de ne pas manquer quelque chose avant de tenter le coup.

Le plugin Babel n'est pas spécifique à l'environnement. J'aimerais que ça continue. Il ne sait rien des modules ou du mécanisme de propagation des mises à jour. Ceux-ci diffèrent selon le bundler.

Par exemple, dans Metro, il n'y a pas du tout de transformation try / finally wrapping. Au lieu de cela, j'ai mis try / finally dans le runtime du

Vous pouvez bien sûr créer un autre plugin Babel pour le wrapping. Mais cela ne vous achète rien en faisant cela via Webpack. Puisque c'est de toute façon spécifique au webpack. Et il peut être déroutant que vous puissiez accidentellement exécuter ce plugin Babel dans un autre environnement (pas Webpack) où cela n'aurait pas de sens.

Vous pouvez, en vous connectant au crochet en cascade compilation.mainTemplate.hooks.require . L'appel précédent de celui-ci est le corps par défaut de la fonction __webpack_require__ , vous pouvez donc taper dans le hook pour envelopper le contenu dans un bloc try/finally .

Le problème est d'obtenir une référence à React dans le __webpack_require__ . C'est possible, mais cela peut nécessiter un certain degré de réentrance et de gardes de récursivité.

Pour plus de détails, vérifiez MainTemplate.js et web/JsonpMainTemplatePlugin.js dans le code source du webpack. JsonpMainTemplatePlugin lui-même puise dans un tas d'hameçons de MainTemplate.js donc c'est probablement la «viande» que vous devez affronter.

Voici un prototype farfelu que j'ai piraté ensemble qui fait efficacement ce que Dan a décrit ci-dessus. C'est malheureusement incomplet, mais prouve une implémentation lo-fi dans webpack: https://gist.github.com/maisano/441a4bc6b2954205803d68deac04a716

Quelques notes:

  • react-dom est codé en dur ici, donc cela ne fonctionnerait pas avec des moteurs de rendu personnalisés ou des sous-packages (par exemple react-dom/profiling ).
  • Je n'ai pas examiné trop profondément le fonctionnement de toutes les variantes de modèles de Webpack, mais la façon dont j'ai enveloppé l'exécution du module est assez haceuse. Je ne suis pas certain que cet exemple fonctionne si, par exemple, on utilise la cible de bibliothèque umd .

Le problème est d'obtenir une référence à React dans le __webpack_require__. C'est possible, mais cela peut nécessiter un certain degré de réentrance et de gardes de récursivité.

Je suppose que vous voulez dire obtenir une référence à Refresh Runtime.

Dans Metro, j'ai résolu ce problème en faisant require.Refresh = RefreshRuntime plus tôt possible. Ensuite, dans l'implémentation require je peux lire une propriété sur la fonction require elle-même. Il ne sera pas disponible immédiatement mais peu importe si nous le configurons suffisamment tôt.

@maisano J'ai dû changer un certain nombre de choses, et finalement je ne vois pas la fonction .accept appelée par webpack. J'ai essayé à la fois .accept(module.i, () => {}) et .accept(() => {}) (auto-acceptant, sauf que cela ne fonctionne pas dans Webpack). La propriété hot est activée, je la vois descendre et parcourir les modules acceptés.

J'ai donc fini par patcher webpack pour appeler des modules auto-acceptables, et c'était la solution finale.

Voici le patch:

diff --git a/node_modules/webpack/lib/HotModuleReplacement.runtime.js b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
index 5756623..7e0c681 100644
--- a/node_modules/webpack/lib/HotModuleReplacement.runtime.js
+++ b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
@@ -301,7 +301,10 @@ module.exports = function() {
                var moduleId = queueItem.id;
                var chain = queueItem.chain;
                module = installedModules[moduleId];
-               if (!module || module.hot._selfAccepted) continue;
+               if (!module || module.hot._selfAccepted) {
+                   module && module.hot._selfAccepted()
+                   continue;
+               }
                if (module.hot._selfDeclined) {
                    return {
                        type: "self-declined",

Je sais que cela va à l'encontre de leur API, qui veut que ce soit un "errorCallback", je me souviens avoir rencontré cela il y a plusieurs années spécifiquement en travaillant sur notre HMR interne, et finalement nous avons fini par écrire notre propre bundler. Je pense que le colis prend en charge l'API de rappel "auto-acceptée". Cela vaut peut-être la peine d'ouvrir un numéro sur Webpack et de voir si nous pouvons le fusionner? @sokra

Alors ... j'ai encore peaufiné le plugin basé sur le travail de @maisano :
https://github.com/pmmmwh/react-refresh-webpack-plugin
(Je l'ai écrit en TypeScript parce que je ne me fais pas confiance en jouant avec les composants internes de Webpack quand j'ai commencé, je peux le convertir en JS / Flow)

J'ai essayé de supprimer le besoin d'un chargeur pour injecter le code hot-module avec les classes webpack Dependency , mais apparemment cela nécessitera une nouvelle analyse de tous les modules (car même avec toutes les fonctions en ligne, nous avons toujours besoin d'un référence à react-refresh/runtime quelque part).

Un autre problème est qu'il n'y a pas de moyen simple (afaik) de détecter des fichiers de type JavaScript dans webpack - par exemple html-webpack-plugin utilise également le type javascript/auto , donc j'ai codé en dur ce qui semble être un masque de fichier acceptable (JS / TS / Flow) pour l'injection du chargeur.

J'ai également ajouté une récupération d'erreur (au moins une erreur de syntaxe) basée sur le commentaire de @gaearon dans ce fil de 5 ans . La prochaine étape consiste à récupérer des erreurs de réaction - je pense que cela peut être fait en injectant une limite d'erreur globale (un peu comme AppWrapper de react-hot-loader ), qui abordera également l'interface de la boîte d'erreur, mais ne l'a pas fait avoir le temps d’y arriver tout de suite.

Le problème soulevé par @natew est également évité - obtenu en découplant l'appel enqueueUpdate et l'appel hot.accpet(errorHandler) .

@pmmmwh Quel timing! Je viens de créer un référentiel qui s'appuie sur / peaufine un peu le travail que j'avais partagé dans l'essentiel.

Je n'ai de toute façon pas abordé la gestion des erreurs, bien que le plugin ici soit un peu plus solide que l'approche initiale que j'avais adoptée.

La prochaine étape consiste à récupérer des erreurs de réaction - je suppose que cela peut être fait en injectant une limite d'erreur globale (un peu comme AppWrapper de react-hot-loader), qui abordera également l'interface de la boîte d'erreur, mais n'a pas eu le temps de cela tout à fait encore.

Cela devrait déjà fonctionner dès le départ. Pas besoin de limite d'erreur personnalisée ou d'encapsulation ici.

La prochaine étape consiste à récupérer des erreurs de réaction - je suppose que cela peut être fait en injectant une limite d'erreur globale (un peu comme AppWrapper de react-hot-loader), qui abordera également l'interface de la boîte d'erreur, mais n'a pas eu le temps de cela tout à fait encore.

Cela devrait déjà fonctionner dès le départ. Pas besoin de limite d'erreur personnalisée ou d'encapsulation ici.

@gaearon Strange. J'ai essayé de lancer des erreurs dans le rendu des composants de la fonction - si l'erreur se produit dans return , HMR fonctionne, mais si cela se produit ailleurs, cela ne fonctionnera pas toujours.

@pmmmwh Quel timing! Je viens de créer un référentiel qui s'appuie sur / peaufine un peu le travail que j'avais partagé dans l'essentiel.

Je n'ai de toute façon pas abordé la gestion des erreurs, bien que le plugin ici soit un peu plus solide que l'approche initiale que j'avais adoptée.

@maisano Que dois-je dire? En fait, j'ai commencé à travailler là-dessus et je suis resté coincé avec le problème d'injection de dépendances le week-end dernier ... Votre essence m'a fourni la solution: tada:

si l'erreur se produit en retour, HMR fonctionne, mais si elle se produit ailleurs, cela ne fonctionnera pas toujours.

J'aurais besoin de plus de détails sur ce que vous avez essayé exactement et ce que vous entendez par «fonctionne» et «ne fonctionne pas».

Il y a beaucoup de choses qui peuvent mal tourner si l'intégration du module bundler n'est pas implémentée correctement (qui est le sujet ou ce fil). Je m'attendrais à ce qu'il n'y ait rien dans React lui-même qui empêche la récupération des erreurs introduites lors des modifications. Vous pouvez vérifier qu'il fonctionne dans React Native 0.61 RC3.

@pmmmwh , @maisano la vérification suivante ignore les modules avec des composants en tant

https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/master/src/loader/utils/isReactRefreshBoundary.ts#L23 -L27

const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
if (desc && desc.get) {
  // Don't invoke getters as they may have side effects.
  return false;
}

Je ne sais pas pourquoi cela est nécessaire dans Metro , mais dans webpack getters renvoient simplement les exportations nommées et pour autant que je sache, il n'y a pas d'effets secondaires. Il devrait donc être sûr de le supprimer.

Les composants @gaearon React.lazy (par exemple les routes partagées en code) ne sont pas rendus. Est-ce par conception? Je peux voir que la limite de rafraîchissement est établie mais performReactRefresh() ne semble rien faire. Les changements apportés aux enfants paresseux s'actualisent bien, donc ce n'est pas un gros problème, mais je me demande si nous faisons quelque chose de mal ...

lazy est une petite machine à états - elle contient une référence à l'ancien composant, et cette référence doit être mise à jour.
Imaginons maintenant que c'était le cas, et qu'il se réfère maintenant à un tout nouvel objet lazy ) - il devra à nouveau penser loading phase

Je m'attendrais à ce que la paresseux travaille. Peut-être que quelque chose s'est cassé. J'ai besoin de voir un cas de reproduction.

Puisqu'il y a eu quelques prototypes, devrions-nous en choisir un et ensuite déplacer cette discussion vers ses problèmes? Et itérer là.

Il y a:
https://github.com/maisano/react-refresh-plugin
et:
https://github.com/pmmmwh/react-refresh-webpack-plugin
J'ai mis en place un fork du plugin pmmmwh qui fonctionne avec [email protected] (corrige également les exportations nommées):
https://github.com/WebHotelier/webpack-fast-refresh

Et pour react-hot-loader ?

react-hot-loader rétroporté presque toutes les fonctionnalités de fast refresh , mais il y a peu de moments historiques et d'intégration, qui ne laissent pas tout rétroporter, et, honnêtement, il n'y a aucun sens de les réimplémenter en termes "rhl" . Alors - laissez-le prendre sa retraite.

J'aurais besoin de plus de détails sur ce que vous avez essayé exactement et ce que vous entendez par «fonctionne» et «ne fonctionne pas».

Il y a beaucoup de choses qui peuvent mal tourner si l'intégration du module bundler n'est pas implémentée correctement (qui est le sujet ou ce fil). Je m'attendrais à ce qu'il n'y ait rien dans React lui-même qui empêche la récupération des erreurs introduites lors des modifications. Vous pouvez vérifier qu'il fonctionne dans React Native 0.61 RC3.

Après quelques ajustements, je peux vérifier que cela fonctionne.

Cependant - il semble que le plugin babel ne fonctionnait pas pour les classes. J'ai vérifié et cela semble se produire indépendamment de l'implémentation, car tout le code injecté et react-refresh/runtime fonctionnent correctement. Je ne sais pas si cela est prévu ou si c'est spécifique au Webpack, si c'est ce dernier, je peux essayer de trouver un correctif demain. (J'ai également testé cela avec uniquement le préréglage métro, reproduisez l'essentiel ici )

Je suis un peu sûr que cela fonctionne pour RN, mais sur ma machine actuelle, je n'ai pas d'environnement pratique à tester sur RN, donc si vous pouvez me diriger vers l'implémentation du plugin babel dans le métro ou les transformations qui seraient vraiment utiles.

Puisqu'il y a eu quelques prototypes, devrions-nous en choisir un et ensuite déplacer cette discussion vers ses problèmes? Et itérer là.

Peut-être pouvons-nous aller ici ? Depuis mon dernier commentaire, j'ai porté l'ensemble du projet dans JS brut et ajouté quelques correctifs sur la mise en file d'attente des mises à jour. Je n'ai pas eu à porter le plugin pour webpack @ 5 , mais après avoir lu le fork de @apostolos et la nouvelle logique HMR dans webpack @ next , les correctifs devraient être simples.

Oui, le plugin Babel n'enregistrera pas les classes. L'intention est que cela se produise au niveau du système du module. Chaque exportation doit être vérifiée pour être «probablement» un composant React. (Une fonction de vérification est fournie par le runtime.) Si true, enregistrez l'exportation, comme le ferait le plugin Babel. Donnez-lui un identifiant comme filename exports%export_name . C'est ce qui fait fonctionner les classes dans Metro, car le plugin Babel ne les trouvera pas.

En d'autres termes, puisque nous ne pouvons pas conserver l'état des classes de toute façon, nous pourrions aussi bien confier leur «localisation» à la limite des exportations du module au lieu d'essayer de les trouver en ligne dans le code source avec une transformation. Les exportations devraient agir comme un «fourre-tout» pour les composants que nous n'avons pas trouvés avec le plugin.

Mailchimp a commencé à utiliser un fork du plugin que j'ai partagé en dernier. Il a été un peu plus étoffé et les gens qui ont choisi de l'utiliser semblent plutôt heureux. Nous allons continuer à l'itérer localement. Je prévois de supprimer le fork et de publier des mises à jour en amont une fois que ce sera un peu plus loin.

@gaearon Réflexions sur l'ajout d'un symbole que nous pouvons attacher à des choses dont nous savons qu'elles peuvent être actualisées sans danger, mais ne sont-elles pas des composants? Par exemple, nous avons un modèle comme:

export default create({
  id: '100'
})

export const View = () => <div />

create renvoie simplement un objet. Je l'ai corrigé pour l'instant de mon côté, mais nous pourrions facilement ajouter un symbole à l'objet d'exportation par défaut qui indique qu'il s'agit d'un fichier sûr. Je ne suis pas sûr du meilleur modèle exactement.

Edit: j'ai réalisé que cela pouvait entrer dans l'implémentation de rafraîchissement! Je pensais que ce serait peut-être mieux dans l'exécution, mais peut-être pas. Avec autant d'implémentations différentes du chargeur, il peut être plus agréable d'avoir une méthode standard.

Avançons de 10 ans. À quoi ressemble votre base de code? Autoriser ici, interdire là? Comment garder ces drapeaux à jour? Comment raisonner? Comme il y a des emplacements _safe to update_, et _ unsafe_, vous devez conserver ou ne pouvez pas vous réconcilier correctement pour une raison quelconque. Quelles raisons dans chaque cas sont des raisons valables?

  • qui symbols vous aurez plus - environ force allow recharger, ou force disallow recharger
  • pourquoi vous pourriez vouloir abaisser la limite de propagation de la mise à jour (c.-à-d. accepter la mise à jour sur "cette" limite de module), ou vouloir l'augmenter (c.-à-d. accepter la mise à jour sur "cette" limite de module)
  • que se passerait-il si aucune limite n'était fixée? S'agit-il uniquement d'un problème de performances ou quelque chose de plus grave pourrait se produire?

Salut les gens 👋 Je cherche à donner un coup de main ici. Avons-nous convenu d'un seul repo / effort?

Est-ce ce repo partagé par @pmmmwh?
https://github.com/pmmmwh/react-refresh-webpack-plugin

Ou est-ce ce repo partagé par @maisano?
https://github.com/maisano/react-refresh-plugin

On dirait que celui de @pmmmwh a été engagé plus récemment. Sauf si j'entends le contraire, je vais supposer que c'est celui sur lequel se concentrer.

La mise en œuvre dans Parcel 2 a commencé ici: https://github.com/parcel-bundler/parcel/pull/3654

Été!

Pour ceux qui le recherchent, une implémentation de React Refresh pour les projets Rollup utilisant Nollup pour le développement: https://github.com/PepsRyuu/rollup-plugin-react-refresh

Probablement pas la mise en œuvre la plus propre, mais cela fonctionne.

Pour les solutions Webpack, il semble qu'il n'y ait pas eu de version officielle des plugins ci-dessus, il semble donc que la meilleure solution HMR pour react soit toujours la bibliothèque de Dan: https://github.com/gaearon/react-hot-loader

Nous venons d'expédier le colis 2 alpha 3 avec prise en charge de Fast Refresh prêt à l'emploi! N'hésitez pas à l'essayer. 😍 https://twitter.com/devongovett/status/1197187388985860096?s=20

🥳 ajout d'une note d'obsolescence à RHL 🥳

Une recette que j'ai utilisée pour essayer ceci sur les applications de l'ARC en utilisant le travail en cours de @pmmmwh , react-app-rewired et customize-cra :

npx create-react-app <project_dir> --typescript

npm install -D react-app-rewired customize-cra react-refresh babel-loader https://github.com/pmmmwh/react-refresh-webpack-plugin

Modifier ./package.json :

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

Ajoutez ./config-overrides.js fichier

// eslint-disable-next-line
const { addBabelPlugin, addWebpackPlugin, override } = require('customize-cra');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

/* config-overrides.js */
module.exports = override(
  process.env.NODE_ENV === 'development'
    ? addBabelPlugin('react-refresh/babel')
    : undefined,
  process.env.NODE_ENV === 'development'
    ? addWebpackPlugin(new ReactRefreshPlugin())
    : undefined,
);

Profiter de l'expérience jusqu'à présent. Merci pour tout le travail de toutes les personnes impliquées!

Merci @ drather19 !

J'ai créé un référentiel basé sur vos instructions, cela fonctionne:https://github.com/jihchi/react-app-rewired-react-refreshSi quelqu'un souhaite essayer et enregistrer un peu de frappe, n'hésitez pas à cloner le dépôt.


Veuillez vous référer à https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/examples/cra-kitchen-sink

ET ... v0.1.0 pour Webpack vient d'être expédié 🎉

@ drather19 @jihchi
Vous voudrez peut-être passer à cette version - elle inclut une expérience plus unifiée ainsi que de nombreuses corrections de bogues sur l'implémentation initiale.

@pmmmwh prend en charge ts-loader + babel-loader ?

@pmmmwh prend en charge ts-loader + babel-loader ?

J'ai fait des tests contre TS avec Babel uniquement et cela fonctionne, donc si cela ne fonctionne pas lorsque vous utilisez les chargeurs ts + babel, n'hésitez pas à signaler un problème :)

@ drather19 J'ai essayé de cloner et d'exécuter votre dépôt mais le serveur de développement ne démarre jamais.

Environnement,
Système d'exploitation - OSX 10.14.6
Nœud - v12.13.0
Fil -1.19.2

@pmmmwh - Pour

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...


@ drather19 J'ai essayé de cloner et d'exécuter votre dépôt mais le serveur de développement ne démarre jamais.

Environnement,
Système d'exploitation - OSX 10.14.6
Nœud - v12.13.0
Fil -1.19.2

@pmmmwh - Pour

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...

Ceci est corrigé dans la branche master du plugin, et sera publié demain.

J'ai réussi à faire fonctionner le plugin webpack de @pmmmwh avec une application TypeScript React utilisant babel. Cependant, les builds incrémentiels prennent environ 12 secondes au lieu de ~ 2 secondes avec juste ts-loader. Je vais continuer à jouer avec cela pour voir s'il me manque quelque chose du côté de la configuration de babel qui rend les performances plus proches, mais pour l'instant, c'est un lavage par rapport à ts-loader et à des rafraîchissements complets.

@IronSean Veuillez le signaler dans le repo de ce plugin? Cela ne semble pas normal.

Je vais continuer à jouer avec cela pour voir s'il me manque quelque chose du côté de la configuration de babel qui rend les performances plus proches, mais pour l'instant, c'est un lavage par rapport à ts-loader et à des rafraîchissements complets.

Ça vous dérange de poster votre configuration / setup là-bas? Je ne pourrai pas comprendre les problèmes sans plus de contexte.

@pmmmwh J'ai ouvert ce numéro pour déplacer la discussion vers votre dépôt une fois que j'ai confirmé que c'était bien votre plugin qui faisait la différence:
https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/20

Est-ce que react-refresh ( React Fast Refresh ?) Fonctionnera avec Preact, ou est-ce que react-hot-loader la solution à long terme pour Preact?

@Jumblemuddle qui dépend de Preact mais ils devraient pouvoir s'intégrer à Fast Refresh s'ils le souhaitent.

Pour les gens de l'ARC qui souhaitent fonctionner avec Fast Refresh, j'ai eu plus de chance avec craco (vs react-app-rewired + custom-cra) maintenant via le craco.config.js :

// eslint-disable-next-line
const { whenDev } = require('@craco/craco');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

module.exports = {
  webpack: {
    configure: webpackConfig => {
      if (process.env.NODE_ENV === 'development') {
        webpackConfig.module.rules.push({
          test: /BabelDetectComponent\.js/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                plugins: [require.resolve('react-refresh/babel')],
              },
            },
          ],
        });
        webpackConfig.module.rules.push({
          test: /\.[jt]sx?$/,
          exclude: /node_modules/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                presets: [
                  '@babel/react',
                  '@babel/typescript',
                  ['@babel/env', { modules: false }],
                ],
                plugins: [
                  '@babel/plugin-proposal-class-properties',
                  '@babel/plugin-proposal-optional-chaining',
                  '@babel/plugin-proposal-nullish-coalescing-operator',
                  'react-refresh/babel',
                ],
              },
            },
          ],
        });
      }
      return webpackConfig;
    },
    plugins: [
      ...whenDev(
        () => [new ReactRefreshPlugin({ disableRefreshCheck: false })],
        [],
      ),
    ],
  },
};

En particulier, l'ajout de webpackConfig.optimization.runtimeChunk = false; vous permettra d'ajouter / supprimer des hooks et de toujours rafraîchir gracieusement.

Profiter encore plus de l'expérience améliorée maintenant. Merci à @ mmhand123 pour le tuyau via https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/25! (<- résolu!)

Sur la base de la suggestion de @ drather19, j'ai publié un plugin de personnalisation pour le rendre plus facile. Voir esetnik / personnaliser-cra-react-refresh .

Merci à @ drather19 je modifie légèrement le code maintenant il peut fonctionner dans une configuration monorepo d'espace de travail de fil.

Tout d'abord, installez les éléments suivants dans les sous-packages pour lesquels vous souhaitez activer l'actualisation rapide:

"@craco/craco": "^5.6.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.2.0", "webpack-hot-middleware": "^2.25.0"

Ajoutez ensuite ceci à craco.config.js :

;(function ForbidCRAClearConsole() {
    try {
        require('react-dev-utils/clearConsole')
        require.cache[require.resolve('react-dev-utils/clearConsole')].exports = () => {}
    } catch (e) {}
})()

const { whenDev } = require('@craco/craco')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
    webpack: {
        configure: webpackConfig => {
            whenDev(() => {
                // Work around monorepo setup when using yarn workspace hoisted packages
                // without the need to list 'babel-loader' and 'babel-preset-react-app' as
                // dependencies to avoid duplication since 'react-scripts' already has them.
                const reactLoaderConfig = webpackConfig.module.rules
                    .find(x => Array.isArray(x.oneOf))
                    .oneOf.find(
                        x =>
                            x.options &&
                            x.options.presets &&
                            x.options.presets.some(p => p.includes('babel-preset-react-app')) &&
                            x.loader &&
                            typeof x.loader.includes === 'function' &&
                            x.loader.includes('babel-loader') &&
                            x.test &&
                            typeof x.test.test === 'function' &&
                            x.test.test('x.tsx') &&
                            x.test.test('x.jsx'),
                    )

                if (reactLoaderConfig) {
                    webpackConfig.module.rules.push({
                        test: /BabelDetectComponent\.js/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })

                    webpackConfig.module.rules.push({
                        test: /\.[jt]sx?$/,
                        exclude: /node_modules/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    presets: reactLoaderConfig.options.presets,
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })
                } else {
                    console.error('cannot find react app loader')
                }

                // console.debug(require('util').inspect(webpackConfig.module.rules, { colors: true, depth: null }))
            })

            return webpackConfig
        },
        plugins: [whenDev(() => new ReactRefreshPlugin({ disableRefreshCheck: false }))].filter(Boolean),
    },
}

@gaearon Nous attendons-nous à ce que Fast Refresh devienne disponible par défaut à l'ARC à un moment donné?
si oui, que faut-il pour cela?

Un peu de travail est nécessaire pour cela :-) qui est actuellement en cours.

si utiliser les fonctions HMR seront appelées? par exemple componentDidMount.
J'utilise react-proxy et componentDidMount sera appelé.
Et React 15.X peut utiliser Fast Refresh?

  • componentDidMount sera appelé. En plus de unmount - les classes seraient rechargées en entier.
  • et c'est le bon moment pour arrêter d'utiliser react-proxy . Eh bien, vous devriez probablement vous arrêter il y a quelques années.
  • 15.X ? - absolument pas. Fast Refresh __ est une partie__ de react, et n'existe donc que dans une version moderne.

nous devrions donc utiliser Fast Refresh ou react-hot-loader pour remplacer react-proxy?
Existe-t-il un moyen d'empêcher les fonctions (componentDidMount) de s'exécuter pour HMR? - il appellera method pour obtenir de nouvelles données.

Comment utiliser React-Hot-Loader dans JIT? - Compilation en temps réel du navigateur

  • nous devrions donc utiliser Fast Refresh ou react-hot-loader pour remplacer react-proxy?

    Essayez d'abord fast refresh , puis RHL

  • Existe-t-il un moyen d'empêcher les fonctions (componentDidMount) de s'exécuter pour HMR? - il appellera method pour obtenir de nouvelles données.

    (utilisez des hooks ...), ne vous fiez pas au cycle de vie des composants, récupérez les données si nécessaire. Essayez react-query , swr ou d'autres solutions.

Quant à la question de savoir comment utiliser le nouveau HMR, je ne pense pas connaître les dernières réflexions à ce sujet. Je vois que @gaearon a un PR wip sur le repo CRA:

Pour clarifier pour les lecteurs, que PR est très dépassé et n'est plus pertinent.

J'ai besoin d'écrire quelque chose sur le fonctionnement de Fast Refresh et comment l'intégrer. Je n'ai pas encore eu le temps.

À ce jour, ce PR est toujours ouvert. Ce serait bien si seuls les PR concernés qui ont encore une chance d'être fusionnés restent ouverts pour avoir une meilleure vue d'ensemble. Si vous les gardez simplement comme référence, je vous recommande de déplacer des éléments vers une branche, une étiquette ou un référentiel différent.

Je continue de recevoir Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. J'ai suivi la documentation mais il semble que j'aie manqué quelque chose?

J'obtiens toujours une erreur: [React Refresh] Hot Module Replacement (HMR) n'est pas activé! React Refresh nécessite HMR pour fonctionner correctement. J'ai suivi la documentation mais il semble que j'aie manqué quelque chose?

@silkfire Je suppose que vous utilisez le plugin webpack. Si oui, veuillez déposer votre question dans le référentiel de plugins webpack: https://github.com/pmmmwh/react-refresh-webpack-plugin/.

À ce jour, ce PR est toujours ouvert. Ce serait bien si seuls les PR concernés qui ont encore une chance d'être fusionnés restent ouverts pour avoir une meilleure vue d'ensemble. Si vous les gardez simplement comme référence, je vous recommande de déplacer des éléments vers une branche, une étiquette ou un référentiel différent.

J'apprécie votre suggestion, mais avec des milliers de notifications non lues, il peut parfois être difficile pour moi de me souvenir de revoir les anciens RP. Je fais confiance aux responsables du référentiel Create React App pour faire ce qu'il faut et fermer s'ils le jugent inutile.

Je vais fermer ça.

Nous avons https://github.com/pmmmwh/react-refresh-webpack-plugin/ comme implémentation de référence pour webpack.
Et https://github.com/facebook/react/issues/16604#issuecomment -528663101 explique comment faire une intégration personnalisée.

Je continue de recevoir Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. J'ai suivi la documentation mais il semble que j'aie manqué quelque chose?

Il semble que vous n'ayez pas activé le Webpack HMR. Pour obtenir de l'aide supplémentaire, veuillez signaler un problème dans le dépôt du plugin.

Comme le remplacement à chaud fait maintenant partie de React - devrait-il avoir une place distincte dans la documentation React, pointant vers les bibliothèques supplémentaires à utiliser avec des bundleurs et des plates-formes particuliers, ainsi que l'explication de certains pièges encore existants, comme avec le CSS à mise à jour automatique modules.

Des informations comme celle-ci ne doivent pas être enterrées dans les problèmes de github et les articles de blog.

@theKashey c'est dans React, mais l'implémentation
De plus, il existe une implémentation de rafraîchissement rapide qui sera fournie avec create-react-app, mais elle n'a pas encore été publiée: pmmmwh / react-refresh-webpack-plugin # 7. Ce sera peut-être dans la prochaine version de react-scripts.

Il est donc probable que l'équipe React ne pense pas encore qu'il soit juste de parler de Fast Refresh pour react-dom dans cette phase expérimentale.

c'est dans React, mais l'implémentation de react-dom n'est qu'expérimentale, par exemple.

Pour être clair, l'implémentation dans react-dom elle-même est stable, tout comme dans React Native. C'est juste que les intégrations ne sont pas toutes stables.

devrait-il avoir une place distincte dans la documentation React, pointant vers les bibliothèques supplémentaires à utiliser avec des bundlers et des plates-formes particuliers, ainsi que l'explication de certains pièges encore existants, comme avec les modules css à mise à jour automatique.

Cela semble raisonnable. Je serais heureux de prendre un PR en l'ajoutant à la section Guides avancés, peut-être basé sur une page RN similaire .

@gaearon
Mon application React est compatible avec certains changements de composants stylisés et applique correctement ces modifications sans aucun problème.
Cependant, lorsque je change du code dans un réducteur Redux, toute l'application est mise à jour en dur et perd tous les états de redux.
Dois-je utiliser d'autres bibliothèques comme redux-persist pour enregistrer l'état actuel avec react-fast-refresh ?

Nous avons bouclé la boucle et c'est reparti 😅

C'est ainsi que fonctionne le HMR de bas niveau et ne relève pas de la responsabilité d'actualisation rapide. Veuillez vous référer à la documentation redux ou webpack

Nous avons bouclé la boucle et c'est reparti 😅

C'est ainsi que fonctionne le HMR de bas niveau et ne relève pas de la responsabilité d'actualisation rapide. Veuillez vous référer à la documentation redux ou webpack

Souhaitez-vous lier la référence du cercle complet?

@ jwchang0206 Assurez-vous d'avoir un code comme celui-ci dans votre magasin.

Souhaitez-vous lier la référence du cercle complet?

Les mêmes questions ont été posées pour React Hot Loader. Les mêmes réponses ont été données. Nous sommes au début d'un nouveau cycle.

@ jwchang0206 Regardez redux- reducers-injector , une petite bibliothèque que j'ai écrite pour résoudre ce problème.
Il vous permettra de prendre en charge le rechargement des réducteurs avec un rechargement à chaud.
Assurez-vous de suivre les principes de redux d'immuabilité dans vos réducteurs et cela fonctionnera sans heurts 💯
Et si vous utilisez des sagas, vous pouvez utiliser redux-sagas-injector .

@gaearon Je suis un peu confus par l'utilisation de window . Cela ne me semble pas vraiment nécessaire parce que l'implémentation est remplacée? À quoi ça sert?

var prevRefreshReg = window.$RefreshReg$; // these are dummies
var prevRefreshSig = window.$RefreshSig$; // these are dummies
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) =>{ /*...*/ }
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {
  // ...
} finally {
  window.$RefreshReg$ = prevRefreshReg; // these are dummies again
  window.$RefreshSig$ = prevRefreshSig; // these are dummies again
}

J'ai mon propre bundler personnalisé et je suis en train de l'implémenter, mais je ne vois pas pourquoi ce serait un must absolu ou quel serait l'intérêt de celui-ci ... au départ, je soupçonnais une optimisation de l'utilisation de la mémoire / des fuites, mais ceux-ci sont juste des appels transférés vers le RefreshRuntime ...

@leidegre Je ne peux pas commenter la décision de définir $ RefreshSig $ sur l'objet window, mais le couplage à un environnement de navigateur m'a posé des problèmes lors de l'utilisation de Fast Refresh dans React NativeScript. @pmmmwh est venu à la rescousse en adaptant son plugin Fast Refresh Webpack pour surmonter le couplage de Fast Refresh au navigateur (les problèmes rencontrés et surmontés ont été discutés dans ce fil: https://github.com/pmmmwh/react-refresh-webpack-plugin/ numéros / 79). Je me demande si l'approche utilisée vous serait utile dans l'intégration de Fast Refresh dans votre bundler personnalisé.

Mon bundler est principalement un wrapper autour du compilateur TypeScript. L'implémentation est principalement celle-ci, adaptée du visiteur react-refresh/babel .

C'est juste une chose simple qui fonctionne mais qui n'est pas aussi complète que le visiteur react-refresh/bable .

import ts = require("typescript")

import { IndexModule } from "./registry"

/** Enables the use of `react-refresh` for hot reloading of React components. */
export function hotTransform(m: IndexModule, hot: boolean) {
  // see https://github.com/facebook/react/issues/16604#issuecomment-528663101
  return (ctx: ts.TransformationContext) => {
    return (sourceFile: ts.SourceFile) => {
      const refreshRuntime = ts.createUniqueName("ReactRefreshRuntime")

      const createSignatureFunctionForTransform = ts.createPropertyAccess(
        refreshRuntime,
        "createSignatureFunctionForTransform"
      )

      const register = ts.createPropertyAccess(refreshRuntime, "register")

      let hasComponents = false

      function visitor(node: ts.Node): ts.VisitResult<ts.Node> {
        if (ts.isFunctionDeclaration(node)) {
          if (_hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
            // assert component naming convention

            if (node.name === undefined) {
              console.warn("unsupported export of unnamed function in ...")
              return node
            }

            const name = node.name
            if (!_isComponentName(name.text)) {
              console.warn(
                `warning: unsupported export '${name.text}' in ${m.path} (${m.id}) does not look like a function component, component names start with a capital letter A-Z. TSX/JSX files should only export React components.`
              )
              return node
            }

            if (!hot) {
              return node // opt-out
            }

            hasComponents = true

            let hookSignatureString = ""

            function hookSignatureStringVisitor(
              node: ts.Node
            ): ts.VisitResult<ts.Node> {
              const hookSig = _getHookSignature(node)
              if (hookSig !== undefined) {
                if (0 < hookSignatureString.length) {
                  hookSignatureString += "\n"
                }
                hookSignatureString += hookSig
              }
              return node
            }

            // update function body to include the call to create signature on render

            const signature = ts.createUniqueName("s")

            node = ts.visitEachChild(
              node,
              (node) => {
                if (ts.isBlock(node)) {
                  return ts.updateBlock(
                    ts.visitEachChild(node, hookSignatureStringVisitor, ctx),
                    [
                      ts.createExpressionStatement(
                        ts.createCall(signature, undefined, [])
                      ),
                      ...node.statements,
                    ]
                  )
                }
                return node
              },
              ctx
            )

            const signatureScope = ts.createVariableStatement(
              undefined,
              ts.createVariableDeclarationList(
                [
                  ts.createVariableDeclaration(
                    signature,
                    undefined,
                    ts.createCall(
                      createSignatureFunctionForTransform,
                      undefined,
                      undefined
                    )
                  ),
                ],
                ts.NodeFlags.Const
              )
            )

            const createSignature = ts.createExpressionStatement(
              ts.createCall(signature, undefined, [
                name,
                ts.createStringLiteral(hookSignatureString),
              ])
            )

            const registerComponent = ts.createExpressionStatement(
              ts.createCall(register, undefined, [
                name,
                ts.createStringLiteral(m.path + " " + name.text),
              ])
            )

            return [signatureScope, node, createSignature, registerComponent]
          }
        }

        if (!hot) {
          // if hot reloading isn't enable, remove hot reloading API calls
          if (ts.isExpressionStatement(node)) {
            const call = node.expression
            if (ts.isCallExpression(call)) {
              if (
                _isPropertyAccessPath(
                  call.expression,
                  "module",
                  "hot",
                  "reload"
                )
              ) {
                return undefined
              }
            }
          }
        }

        return node
      }

      sourceFile = ts.visitEachChild(sourceFile, visitor, ctx)

      if (hot && hasComponents) {
        let reactIndex = sourceFile.statements.findIndex((stmt) => {
          if (ts.isImportEqualsDeclaration(stmt)) {
            const ref = stmt.moduleReference
            if (ts.isExternalModuleReference(ref)) {
              const lit = ref.expression
              if (ts.isStringLiteral(lit)) {
                return lit.text === "react"
              }
            }
          }
          return false
        })

        if (reactIndex === -1) {
          console.warn(`cannot find import React = require('react') in ...`)
          reactIndex = 0
        }

        // insert after

        sourceFile = ts.updateSourceFileNode(sourceFile, [
          ...sourceFile.statements.slice(0, reactIndex + 1),
          ts.createImportEqualsDeclaration(
            undefined,
            undefined,
            refreshRuntime,
            ts.createExternalModuleReference(
              ts.createStringLiteral("react-refresh/runtime")
            )
          ),
          ...sourceFile.statements.slice(reactIndex + 1),
          ts.createExpressionStatement(
            ts.createCall(
              ts.createPropertyAccess(
                ts.createPropertyAccess(
                  ts.createIdentifier("module"),
                  ts.createIdentifier("hot")
                ),
                ts.createIdentifier("reload")
              ),
              undefined,
              undefined
            )
          ),
          ts.createExpressionStatement(
            ts.createBinary(
              ts.createPropertyAccess(
                ts.createIdentifier("globalThis"),
                ts.createIdentifier("__hot_enqueueUpdate")
              ),
              ts.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
              ts.createCall(
                ts.createPropertyAccess(
                  ts.createIdentifier("globalThis"),
                  ts.createIdentifier("__hot_enqueueUpdate")
                ),
                undefined,
                undefined
              )
            )
          ),
        ])
      }

      return sourceFile
    }
  }
}

function _hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
  const modifiers = node.modifiers
  if (modifiers !== undefined) {
    for (let i = 0; i < modifiers.length; i++) {
      if (modifiers[i].kind === kind) {
        return true
      }
    }
  }
  return false
}

function _isComponentName(name: string): boolean {
  // ^[A-Z]
  const ch0 = name.charCodeAt(0)
  return 0x41 <= ch0 && ch0 <= 0x5a
}

function _isPropertyAccessPath(
  node: ts.Expression,
  ...path: ReadonlyArray<string>
): node is ts.PropertyAccessExpression {
  for (let i = 0; i < path.length; i++) {
    if (ts.isPropertyAccessExpression(node)) {
      if (!(node.name.text === path[path.length - (i + 1)])) {
        return false
      }
      node = node.expression
    }
  }
  return true
}

function _getHookSignature(node: ts.Node): string | undefined {
  if (ts.isExpressionStatement(node)) {
    const call = node.expression
    if (ts.isCallExpression(call)) {
      const prop = call.expression
      if (ts.isPropertyAccessExpression(prop)) {
        const text = prop.name.text
        if (text.startsWith("use") && 3 < text.length) {
          // todo: add additional checks and emit warnings if the hook usage looks non standard

          return text
        }
      }
    }
  }
  return undefined
}

Je ne savais pas trop comment utiliser createSignatureFunctionForTransform au début, mais c'est juste une fonction d'usine qui crée une petite machine à états pour chaque composant. Vous l'appelez donc une fois pour chaque fonction avec la signature de hook statique (qui est juste une valeur opaque, semblable à un hachage). Vous l'appelez ensuite à partir du rendu pour qu'il termine son travail de configuration.

Cela change quelque chose comme ceci:

import React = require("react")

export function App() {
  const [state, setState] = React.useState(0)

  return (
    <React.Fragment>
      <p>
        Click Count !!!<strong>{state}</strong>!!!
        <br />
        <button onClick={() => setState((acc) => acc + 1)}>Click me</button>
      </p>
    </React.Fragment>
  )
}

Dans ceci:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const React = require("react");
const ReactRefreshRuntime_1 = require(6);
const s_1 = ReactRefreshRuntime_1.createSignatureFunctionForTransform();
function App() {
    s_1();
    const [state, setState] = React.useState(0);
    return (React.createElement(React.Fragment, null,
        React.createElement("p", null,
            "Click Count !!!",
            React.createElement("strong", null, state),
            "!!!",
            React.createElement("br", null),
            React.createElement("button", { onClick: () => setState((acc) => acc + 1) }, "Click me"))));
}
exports.App = App;
s_1(App, "useState");
ReactRefreshRuntime_1.register(App, "./App App");
module.hot.reload();
globalThis.__hot_enqueueUpdate && globalThis.__hot_enqueueUpdate();

Notez que le visiteur est incomplet. Il ne traite que du cas d'utilisation le plus basique.

Je suis un peu confus par l'utilisation de window . Cela ne me semble pas vraiment nécessaire parce que l'implémentation est remplacée? À quoi ça sert?

@leidegre

Je pense que l'implémentation dans Metro n'utilise pas window mais plutôt la portée réelle global .

Je ne connais pas la justification originale de cette implémentation, mais cela a été utile d'après mon expérience - cela garantit que la logique requise réelle est indépendante de la logique d'actualisation rapide (ce qui signifie que les transformations react-refresh/babel peuvent être utilisées avec pratiquement n'importe quel bundler). Comme pour l'échange, il agit également comme un garde pour garantir que les modules qui ne sont pas censés être traités par le runtime ne le seront pas:

Prenons le cas où @babel/runtime est utilisé, ce qui injectera des helpers sous forme d'importations dans le bundle et vous ne voulez HMR que du code non node_modules . Si vous n'initialisez pas d'abord les helpers vides et que vous affectez encore des helpers à la portée globale, un cas rare peut se produire où les helpers injectés par Babel appelleront cleanup avant que le module user-land ne termine réellement l'initialisation (car ils sont enfants importations).

Cette page vous a été utile?
0 / 5 - 0 notes