J'ai rencontré un motif répété sur mon site react-redux: Un composant affiche les données d'une API Web et il devrait être rempli au chargement, automatiquement, sans aucune interaction de l'utilisateur.
Je veux lancer l'extraction asynchrone à partir d'un composant conteneur, mais pour autant que je sache, le seul moyen de le faire consiste à partir d'un événement de cycle de vie dans un composant d'affichage. Cela semble rendre impossible toute la logique du conteneur et l’utilisation exclusive de composants fonctionnels stupides sans état pour l’affichage.
Cela signifie que je ne peux pas utiliser de composant fonctionnel sans état pour un composant nécessitant des données asynchrones. Cela ne semble pas juste.
Il semble que la "bonne" façon de procéder consiste à lancer des appels asynchrones à partir du conteneur . Ensuite, lorsque l'appel sera renvoyé, l'état sera mis à jour et le conteneur obtiendra le nouvel état et, à son tour, transmettra ceux-ci à son composant sans état via mapStateToProps()
.
Faire des appels asynchrones dans mapStateToProps
et mapDispatchToProps
(je veux dire appeler en fait la fonction async, par opposition à la renvoyer en tant que propriété) n'a pas de sens.
J'ai donc fini par placer le ou les appels asynchrones dans une fonction refreshData()
exposée par mapDispatchToProps()
, puis en l'appelant à partir de deux méthodes de cycle de vie React ou plus: componentDidMount and componentWillReceiveProps
.
Existe-t-il un moyen propre de mettre à jour l'état du magasin redux sans mettre d'appels de méthode de cycle de vie dans chaque composant nécessitant des données asynchrones?
Devrais-je faire ces appels plus haut dans la hiérarchie des composants (réduisant ainsi la portée de ce problème, puisque seuls les composants "de niveau supérieur" auraient besoin d'écouter les événements du cycle de vie)?
Juste pour qu'il n'y ait pas de confusion avec ce que je veux dire par un composant conteneur connect (), voici un exemple très simple:
import React from 'react';
import { connect } from 'react-redux';
import {action} from './actions.js';
import MyDumbComponent from './myDumbComponent.jsx';
function mapStateToProps(state)
{
return { something: state.xxxreducer.something };
}
function mapDispatchToProps(dispatch)
{
return {
doAction: ()=>{dispatch(action())}
};
}
const MyDumbComponentContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MyDumbComponent);
// Uh... how can I hook into to componentDidMount()? This isn't
// a normal React class.
export default MyDumbComponentContainer;
Jamie Dixon a écrit un paquet pour le faire!
https://github.com/JamieDixon/react-lifecycle-component
L'utilisation ressemblerait à ceci:
const mapDispatchToProps = {
componentDidMount: getAllTehDatas
}
...
export default connectWithLifecycle(mapStateToProps, mapDispatchToProps)(WrappedComponent)
edit Avec les points d'ancrage, vous pouvez maintenant implémenter des rappels de cycle de vie dans un composant fonctionnel sans état. Bien que cela puisse ne pas répondre directement à tous les points de la question, cela peut également permettre de contourner certaines des raisons pour lesquelles on souhaite faire ce qui a été proposé à l'origine.
modifier en réponse originale Après la discussion dans les commentaires et y réfléchir plus, cette réponse est plus exploratoire et peut servir de partie de la conversation. Mais je ne pense pas que ce soit la bonne réponse.
réponse originale
Sur le site Redux, il y a un exemple qui montre que vous n'avez pas à faire à la fois mapStateToProps et mapDispatchToProps. Vous pouvez simplement exploiter la génialité de connect
pour les accessoires, utiliser une classe et implémenter les méthodes de cycle de vie sur le composant bête.
Dans l'exemple, l'appel de connexion se trouve même dans le même fichier et le composant dumb n'est même pas exporté. Par conséquent, il est identique pour l'utilisateur du composant.
Je peux comprendre que je ne veuille pas émettre d’appels async. Je pense qu’il ya une distinction entre émettre des appels asynchrones à partir de là et envoyer une action qui, avec thunks, déplace l’émission des appels asynchrones dans les actions (encore plus découplée du code React).
À titre d'exemple, voici un composant d'écran de démarrage dans lequel j'aimerais effectuer une action asynchrone (comme le préchargement d'actifs) lorsque le composant d'affichage est monté:
SplashContainer.js
import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'
const mapStateToProps = (state) => {
return {
// whatever you need here
}
}
const mapDispatchToProps = (dispatch) => {
return {
onMount: () => dispatch(actions.splashMount())
}
}
const SceneSplash = connect(
mapStateToProps,
mapDispatchToProps
)(Splash)
export default SceneSplash
Splash.js
import React from 'react'
class Splash extends React.Component {
render() {
return (
<div className="scene splash">
<span className="fa fa-gear fa-spin"></span>
</div>
)
}
componentDidMount() {
const { onMount } = this.props
onMount()
}
}
export default Splash
Vous pouvez voir que la répartition se produit dans le conteneur connecté et vous pouvez imaginer l'appel actions.splashMount()
émettre une requête http asynchrone ou effectuer d'autres opérations asynchrones via thunks ou promesses.
edit pour clarifier
Permettez-moi d'essayer de défendre l'approche. Je relis la question et je ne suis pas tout à fait sûr d’adresser la question principale, mais tenez compte de moi. Si je ne suis pas encore sur la bonne voie, j'ai une approche modifiée ci-dessous qui peut être plus proche de la marque.
"il devrait être rempli au chargement" - l'exemple ci-dessus accomplit ceci
"Je souhaite lancer l'extraction asynchrone à partir d'un conteneur" - dans l'exemple, elle n'est pas initiée à partir du composant d'affichage ni du conteneur, mais à partir d'une action asynchrone.
"Cela semble rendre impossible toute la logique dans le conteneur" - je pense que vous pouvez toujours mettre toute logique supplémentaire nécessaire dans le conteneur. Comme indiqué précédemment, le code de chargement de données ne se trouve pas dans le composant d'affichage (ou le conteneur), mais dans le créateur de l'action asynchrone.
"Cela signifie que je ne peux pas utiliser de composant fonctionnel sans état pour un composant nécessitant des données asynchrones." - Dans l'exemple ci-dessus, le composant d'affichage est sans état et fonctionnel. Le seul lien est la méthode de cycle de vie appelant un rappel. Il n'a pas besoin de savoir ou de se soucier de ce que ce rappel fait. Ce n'est pas le cas du composant d'affichage qui essaie d'être le propriétaire de l'extraction asynchrone de données. Il s'agit simplement de laisser le code qui gère le système savoir quand un événement particulier s'est produit.
Jusqu'à présent, j'essaie de justifier en quoi l'exemple donné répond aux exigences de la question. Cela dit, si vous recherchez un composant d’affichage qui ne contient absolument aucun code relatif au chargement de données async, même par rappel indirect, c’est-à-dire que le seul lien qu’il possède est de consommer ces données via les accessoires qu’il leur a remis. les données distantes descendent, alors je suggérerais quelque chose comme ceci:
SplashContainer.js
import { connect } from 'react-redux'
import Splash from '../components/Splash'
import * as actions from '../actions'
const mapStateToProps = (state) => {
return {
// whatever you need here
}
}
const mapDispatchToProps = (dispatch) => {
dispatch(actions.splashMount())
return {
// whatever else here may be needed
}
}
const SceneSplash = connect(
mapStateToProps,
mapDispatchToProps
)(Splash)
export default SceneSplash
Splash.js
import React from 'react'
class Splash extends React.Component {
// incorporate any this.props references here as desired
render() {
return (
<div className="scene splash">
<span className="fa fa-gear fa-spin"></span>
</div>
)
}
}
export default Splash
En envoyant l'action dans mapDispatchToProps, vous laissez le code de cette action résident entièrement dans le conteneur. En fait, vous démarrez l'appel asynchrone dès que le conteneur est instancié, plutôt que d'attendre que le composant d'affichage connecté soit mis en rotation et monté. Toutefois, si vous ne pouvez pas commencer l'appel asynchrone avant le composantDidMount () pour le composant d'affichage, je pense que vous êtes intrinsèquement lié au code, comme dans mon premier exemple.
Je n'ai pas réellement testé cette seconde approche pour voir si réagit ou redux s'en plaindra, mais cela devrait fonctionner. Vous avez accès à la méthode d'envoi et devriez pouvoir l'appeler sans problème.
Pour être honnête, ce deuxième exemple, tout en supprimant tout le code lié à l'action asynchrone du composant d'affichage, me semble plutôt drôle, car nous faisons des choses non mappées avec des objets dans le système éponyme. une fonction. Et les conteneurs n'ont pas réellement de composantDidMount pour l'exécuter autrement. Donc, je suis un peu mal à l'aise et je me pencherais vers la première approche. Ce n’est pas propre dans le sens "se sent bien", mais dans le sens "simple 1-liner".
Découvrez redux-saga https://github.com/yelouafi/redux-saga . C'est un composant du middleware redux qui crée des observateurs de longue durée qui recherchent des actions de magasin spécifiques et peuvent déclencher des fonctions ou des fonctions de générateur en réponse. La syntaxe du générateur est particulièrement intéressante pour la gestion asynchrone, et redux-saga dispose de quelques utilitaires Nice permettant de traiter le code asynchrone de manière synchrone. Voir certains de leurs exemples. https://github.com/yelouafi/redux-saga/blob/master/examples/async/src/sagas/index.js . La syntaxe du générateur peut être difficile à comprendre au début, mais selon notre expérience, cette syntaxe prend en charge une logique asynchrone extrêmement complexe, comprenant des demandes multiples anti-rebond, d'annulation et de jointure/course.
Vous pouvez le faire à partir d'un conteneur. Créez simplement un composant qui étend React.Component mais nommez-le "Container" quelque part dans le nom. Puis utilisez ce conteneur composantDidMount au lieu d'utiliser composantDidMount dans le composant de présentation (dumb) rendu par le composant conteneur. Réducteur verra que vous avez toujours envoyé une action et que vous mettez toujours à jour l'état de sorte que votre composant stupide puisse accéder à ces données.
Je TDD mais même si je n'avais pas TDD, je sépare mes composants stupides par rapport aux composants du conteneur via un fichier. Je déteste avoir trop de données dans un fichier, surtout si vous mélangez des contenus idiots ou contenant dans un même fichier, c'est un désastre. Je sais que les gens le font, mais je trouve ça affreux.
Je fais ça:
src/components/someDomainFolder/someComponent.js
(composant bête) src/components/someDomainFolder/someComponentContainer.js
(par exemple, vous pouvez utiliser React-Redux..avoir connecté conteneur pas un composant de présentation connecté..et dans certainsComponentContainer.js vous avez une classe de réaction dans ce fichier, comme indiqué, il suffit de l'appeler someComponentContainer étend React.Component par exemple.
Vos mapStateToProps () et mapDispatchToProps () seraient des fonctions globales de ce composant conteneur connecté en dehors de cette classe de conteneur. Et connect () rendrait le conteneur, ce qui rendrait le composant de présentation, mais cela vous permet de conserver tout votre comportement dans votre fichier de conteneur, à l’abri du code muet du composant de présentation.
de cette façon, vous avez des tests structurels/d'état autour de someComponent et des tests de comportement autour du composant Container. Un meilleur moyen de procéder à la maintenance et à la rédaction des tests, ainsi qu’au maintien et à la simplification de la tâche, pour vous-même ou pour d’autres développeurs, de voir ce qui se passe et de gérer les composants idiot et comportemental.
En faisant les choses de cette façon, votre présentation est séparée physiquement par fichier ET par convention de code. ET vos tests sont regroupés autour des zones de code appropriées ... et non d'un désordre mélangé. Et si vous faites cela, et utilisez un réducteur qui écoute pour mettre à jour l’état, votre composant de présentation peut rester totalement stupide ... et ne cherchez que cet état de mises à jour via des accessoires ... puisque vous utilisez mapStateToProps ().
Pour faire suite à la suggestion de @PositiveGuy, voici un exemple de code décrivant comment implémenter un composant conteneur pouvant utiliser des méthodes de cycle de vie. Je pense que c'est une approche assez propre qui maintient la séparation des préoccupations en gardant le composant de présentation "idiot":
import React from 'react';
import { connect } from 'react-redux'
import { myAction } from './actions/my_action_creator'
import MyPresentationComponent from './my_presentation_component'
const mapStateToProps = state => {
return {
myStateSlice: state.myStateSlice
}
}
const mapDispatchToProps = dispatch => {
return {
myAction: () => {
dispatch(myAction())
}
}
}
class Container extends React.Component {
componentDidMount() {
//You have lifecycle access now!!
}
render() {
return(
<MyPresentationComponent
myStateSlice={this.props.myStateSlice}
myAction={this.props.myAction}
/>
)
}
}
const ContainerComponent = connect(
mapStateToProps,
mapDispatchToProps
)(Container)
export default ContainerComponent