Redux: Utilisation recommandée de connect()

Créé le 7 août 2015  ·  34Commentaires  ·  Source: reduxjs/redux

Salut @gaearon - la déclaration suivante m'a pris au dépourvu lors de la lecture de la documentation :

Ensuite, nous enveloppons les composants que nous voulons connecter à Redux avec la fonction connect() de react-redux. Essayez de le faire uniquement pour un composant de niveau supérieur ou des gestionnaires de routage. Bien que techniquement vous puissiez connecter () n'importe quel composant de votre application au magasin Redux, évitez de le faire trop profondément car cela rendra le flux de données plus difficile à tracer.

Les chaînes d'hélices profondes étaient l'un des problèmes avec React qui m'a amené à utiliser Flux; le compromis pour faciliter le suivi du flux de données est que vous devez configurer/maintenir/tracer le flux d'accessoires, et plus important encore, les parents doivent connaître les exigences en matière de données de leurs enfants. D'après mon expérience, je ne suis pas convaincu que cette approche soit meilleure, mais je suis curieux de savoir ce que vous en pensez :sourire:

docs

Commentaire le plus utile

Existe-t-il d'autres problèmes que le traçage du flux de données lors de la connexion () de nombreux composants ? Y aurait-il une pénalité de performance par exemple ?

Non, c'est exactement le contraire : vous obtenez de meilleures performances en abandonnant le flux descendant.

Tous les 34 commentaires

Je suis tout à fait d'accord avec Brent. C'était une grande partie de la réflexion fondamentale derrière quelque chose comme Relay et conduit à une plus grande cohésion.

Peut-être devrions-nous simplement dire « les gens ont des préférences différentes ici ».

@gaearon - peut-être qu'une section sur les compromis qui comprend une démonstration de la façon dont cela complique le flux de données de traçage serait utile. Je pourrais me ranger de mon côté des préférences pour cela.

Frais. Gardons-le ouvert et revoyons-le une fois que les documents initiaux se sont installés.

Ça sonne bien @gaearon ! :) Envoyez-moi un ping lorsque vous souhaitez revenir

Je suis assez nouveau sur Redux, mais cela a été un gros problème pour moi au cours de la dernière année, en créant une application où nous avons beaucoup changé la structure des données. Je dois donc dire que je suis vraiment d'accord avec @brentvatne sur celui-ci.

Existe-t-il d'autres problèmes que le traçage du flux de données lors de la connexion () de nombreux composants ? Y aurait-il une pénalité de performance par exemple ?

Existe-t-il d'autres problèmes que le traçage du flux de données lors de la connexion () de nombreux composants ? Y aurait-il une pénalité de performance par exemple ?

Non, c'est exactement le contraire : vous obtenez de meilleures performances en abandonnant le flux descendant.

Parce que vous pouvez éviter les re-rendus inutiles des composants de niveau intermédiaire ?

Oui.

Pour ajouter à la discussion : je limite principalement mon utilisation des composants connect to route. Lorsque j'ai une page qui pourrait utiliser un composant intelligent supplémentaire (par exemple un formulaire modal), ma solution de contournement a été de passer l'élément ou le nœud et de faire en sorte que le composant stupide le rende. Cela signifie que vous avez un peu plus de passe-partout, mais tester le composant stupide est toujours facile. Je suis toujours en train d'expérimenter cela, mais je pense que cela pourrait être la meilleure façon de composer des composants intelligents sans renoncer à la testabilité facile.

Pour donner un exemple grossier :

Foo.jsx

export class Foo extends Component {
  render () {
    return (
      <div className='foo'>
        {/* foo stuff going on in here */}
        {this.props.Bar}
      </div>
    )
  }
}

FooContainer.jsx

@connect(getState, getActions)
export class FooContainer extends Component {
  render () {
    return (
      <Foo
        Bar={<BarContainer/>}
        {...this.props}
       />
    )
  }
}

@brentvatne Si vous avez envie d'écrire quelque chose, déterminez où l'

Mise à jour du 19/10/2015 : j'ai amélioré ce commentaire et l'ai publié sous le nom react-redux-provide . Voir https://github.com/rackt/redux/issues/419#issuecomment -149325401 ci-dessous.

Suite de #475, je décrirai ce que j'ai trouvé, bien que cette discussion appartienne probablement maintenant à react-redux . ;)

Fournisseurs modulaires utilisant l'affectation latérale

Pour faire court, l'approche que j'ai adoptée s'articule autour de fournisseurs modulaires que vous pouvez affecter à n'importe quel composant. Cela permet des composants _vraiment_ " stupides ", impose une séparation maximale des préoccupations et permet d'utiliser et de partager très facilement et rapidement un nombre illimité de fournisseurs interchangeables. Il applique également un moyen plus efficace de mettre à jour les composants.

Alors... je me suis débarrassé des répertoires actions , constants , containers , reducers , et stores (commun dans les exemples ) et les a remplacés par un seul répertoire providers . Alternativement, un répertoire providers peut même ne pas être nécessaire, car cette approche permettrait aux fournisseurs autonomes d'être empaquetés et distribués. Je pense que nous verrons des trucs vraiment cool émerger si cette approche particulière est adoptée !! (Remarque : il n'est bien sûr pas nécessaire de regrouper ces répertoires en un seul, mais je pense que 1) rend les choses beaucoup plus faciles à lire/comprendre et 2) réduit le passe-partout et 3) les fournisseurs individuels sont suffisamment petits pour que cela ait du sens. )

Exemple de composant « Dumb »

// components/Branch.js

import React, { Component } from 'react';
import provide from '../utilities/provide.js';
import { branchName, tree, toggle, open, theme } from '../common/propTypes.js';
import Limbs from './Limbs.js';

<strong i="22">@provide</strong>  // maybe require specifying expected props? e.g., @provide('theme')
export default class Branch extends Component {
  static propTypes = { branchName, tree, toggle, open, theme };

  onClick(event) {
    const { branchName, toggle } = this.props;  // toggle is from a provider

    event.stopPropagation();
    toggle(branchName);
  }

  render() {
    const props = this.props;
    const { branchName, tree, open, theme } = props;  // latter 3 from providers
    const classes = theme.sheet.classes || {};
    const imgSrc = open ? 'folder-open.png' : 'folder-closed.png';

    return (
      <div
        onClick={::this.onClick}
        className={classes.branch}
      >
        <h4 className={classes.branchName}>
          <img
            className={classes.branchIcon}
            src={theme.imagesDir+imgSrc}
          />

          <span>{branchName}</span>
        </h4>

        <Limbs
          tree={tree}
          open={open}
        />
      </div>
    );
  }
}

Exemple de fournisseur

// providers/toggle.js

import createProvider from '../utilities/createProvider.js';

export const TOGGLE = 'TOGGLE';

export const actions = {
  toggle(fullPath) {
    return { type: TOGGLE, fullPath };
  }
};

export const reducers = {
  open(state = {}, action) {
    switch (action.type) {
      case TOGGLE:
        const { fullPath } = action;
        return { ...state, [fullPath]: !state[fullPath] };

      default:
        return state;
    }
  }
};

function merge (stateProps, dispatchProps, parentProps) {
  return Object.assign({}, parentProps, {
    open: !!stateProps.open[parentProps.fullPath]
  });
}

export const provider = createProvider(actions, reducers, merge);
export default provider;

Affectation latérale

Comme mentionné, l'idée est de pouvoir facilement affecter des fournisseurs arbitraires à des composants « muets ». Ainsi, lors du montage de votre application, vous pouvez procéder comme ceci :

import React from 'react';
import { Provider } from 'react-redux';

import assignProviders from './utilities/assignProviders.js';
import createStoreFromProviders from './utilities/createStoreFromProviders.js';

import dark from './themes/dark.js';
import github from './sources/github.js';
import * as providers from './providers/index.js';
import * as components from './components/index.js';

const { packageList, sources, toggle, theme } = providers;
const { Branches, Branch, Limbs, Limb } = components;

const initialState = {
  packageList: [
    'github:gaearon/react-redux<strong i="10">@master</strong>',
    'github:loggur/branches<strong i="11">@master</strong>',
    'github:rackt/redux<strong i="12">@master</strong>'
  ],
  sources: {
    github: github({
      token: 'abc123',
      auth: 'oauth'
    })
  },
  open: {
    'github': true,
    'github:gaearon': true,
    'github:gaearon/react-redux<strong i="13">@master</strong>': true,
    'github:rackt': true,
    'github:rackt/redux<strong i="14">@master</strong>': true
  },
  theme: dark
};

const store = createStoreFromProviders(providers, initialState);

assignProviders({ theme }, components);
assignProviders({ packageList }, { Branches });
assignProviders({ sources }, { Branches, Branch });
assignProviders({ toggle }, { Branch, Limb });

React.render(
  <Provider store={store}>
    {() => <Branches/>}
  </Provider>,
  document.getElementById('root')
);

J'espère que tout cela est assez simple, mais je serais heureux d'entrer dans les détails et d'ajouter des commentaires pour des éclaircissements si nécessaire.

Code supplémentaire

Vous remarquerez probablement qu'une poignée de fonctions utilitaires sont importées dans ces exemples. Ce sont de minuscules modules (3 des 4 ne font que quelques lignes) qui ne sont en fait qu'une combinaison des méthodes redux et react-redux existantes, conçues pour les cas d'utilisation les plus courants. Vous n'êtes bien sûr pas limité à ces fonctions. Ils n'existent que pour rendre les choses encore plus faciles.

// utilities/createProvider.js

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

/**
 * Creates an object to be used as a provider from a set of actions and 
 * reducers.
 *
 * <strong i="10">@param</strong> {Object} actions
 * <strong i="11">@param</strong> {Object} reducers
 * <strong i="12">@param</strong> {Function} merge Optional
 * <strong i="13">@return</strong> {Object}
 * <strong i="14">@api</strong> public
 */
export default function createProvider (actions, reducers, merge) {
  return {
    mapState(state) {
      const props = {};

      for (let key in reducers) {
        props[key] = state[key];
      }

      return props;
    },

    mapDispatch(dispatch) {
      return bindActionCreators(actions, dispatch);
    },

    merge
  };
}
// utilities/createStoreFromProviders.js

import { createStore, combineReducers } from 'redux';

import { createStore, applyMiddleware, combineReducers } from 'redux';

/**
 * Creates a store from a set of providers.
 *
 * <strong i="17">@param</strong> {Object} providers
 * <strong i="18">@param</strong> {Object} initialState Optional
 * <strong i="19">@return</strong> {Object}
 * <strong i="20">@api</strong> public
 */
export default function createStoreFromProviders (providers, initialState) {
  const reducers = {};
  const middleware = [];
  let create = createStore;

  for (let key in providers) {
    let provider = providers[key];

    Object.assign(reducers, provider.reducers);

    if (provider.middleware) {
      if (Array.isArray(provider.middleware)) {
        for (let mid of provider.middleware) {
          if (middleware.indexOf(mid) < 0) {
            middleware.push(mid);
          }
        }
      } else if (middleware.indexOf(provider.middleware) < 0) {
        middleware.push(provider.middleware);
      }
    }
  }

  if (middleware.length) {
    create = applyMiddleware.apply(null, middleware)(createStore);
  }

  return create(combineReducers(reducers), initialState);
}
// utilities/assignProviders.js

/**
 * Assigns each provider to each component.  Expects each component to be
 * decorated with `@provide` such that it has an `addProvider` static method.
 *
 * <strong i="5">@param</strong> {Object} providers
 * <strong i="6">@param</strong> {Object} components
 * <strong i="7">@api</strong> public
 */
export default function assignProviders (providers, components) {
  for (let providerName in providers) {
    let provider = providers[providerName];

    if (provider.default) {
      provider = provider.default;
    } else if (provider.provider) {
      provider = provider.provider;
    }

    for (let componentName in components) {
      let addProvider = components[componentName].addProvider;
      if (typeof addProvider === 'function') {
        addProvider(providerName, provider);
      }
    }
  }
}

Et enfin, nous avons le décorateur provide . C'est une version modifiée de connect conçue pour permettre une affectation latérale.

// utilities/provide.js

import React, { Component, PropTypes } from 'react';
import createStoreShape from 'react-redux/lib/utils/createStoreShape';
import shallowEqual from 'react-redux/lib/utils/shallowEqual';
import isPlainObject from 'react-redux/lib/utils/isPlainObject';
import wrapActionCreators from 'react-redux/lib/utils/wrapActionCreators';
import invariant from 'invariant';

const storeShape = createStoreShape(PropTypes);
const defaultMapState = () => ({});
const defaultMapDispatch = dispatch => ({ dispatch });
const defaultMerge = (stateProps, dispatchProps, parentProps) => ({
  ...parentProps,
  ...stateProps,
  ...dispatchProps
});

// Helps track hot reloading.
let nextVersion = 0;

export default function provide (WrappedComponent) {
  const version = nextVersion++;
  const providers = [];
  let shouldSubscribe = false;

  function getDisplayName () {
    return ''
      +'Provide'
      +(WrappedComponent.displayName || WrappedComponent.name || 'Component')
      +'('+providers.map(provider => provider.name).join(',')+')';
  }

  function addProvider (name, { mapState, mapDispatch, merge }) {
    if (Boolean(mapState)) {
      shouldSubscribe = true; 
    }

    providers.push({
      name,
      mapState: mapState || defaultMapState,
      mapDispatch: isPlainObject(mapDispatch)
        ? wrapActionCreators(mapDispatch)
        : mapDispatch || defaultMapDispatch,
      merge: merge || defaultMerge
    });

    Provide.displayName = getDisplayName();
  }

  function computeStateProps (store) {
    const state = store.getState();
    const stateProps = {};

    for (let provider of providers) {
      let providerStateProps = provider.mapState(state);

      invariant(
        isPlainObject(providerStateProps),
        '`mapState` must return an object. Instead received %s.',
        providerStateProps
      );

      Object.assign(stateProps, providerStateProps);
    }

    return stateProps;
  }

  function computeDispatchProps (store) {
    const { dispatch } = store;
    const dispatchProps = {};

    for (let provider of providers) {
      let providerDispatchProps = provider.mapDispatch(dispatch);

      invariant(
        isPlainObject(providerDispatchProps),
        '`mapDispatch` must return an object. Instead received %s.',
        providerDispatchProps
      );

      Object.assign(dispatchProps, providerDispatchProps);
    }

    return dispatchProps;
  }

  function computeNextState (stateProps, dispatchProps, parentProps) {
    const mergedProps = {};

    for (let provider of providers) {
      let providerMergedProps = provider.merge(
        stateProps, dispatchProps, parentProps
      );

      invariant(
        isPlainObject(providerMergedProps),
        '`merge` must return an object. Instead received %s.',
        providerMergedProps
      );

      Object.assign(mergedProps, providerMergedProps);
    }

    return mergedProps;
  }

  const Provide = class extends Component {
    static displayName = getDisplayName();
    static contextTypes = { store: storeShape };
    static propTypes = { store: storeShape };
    static WrappedComponent = WrappedComponent;
    static addProvider = addProvider;

    shouldComponentUpdate(nextProps, nextState) {
      return !shallowEqual(this.state.props, nextState.props);
    }

    constructor(props, context) {
      super(props, context);
      this.version = version;
      this.store = props.store || context.store;

      invariant(this.store,
        `Could not find "store" in either the context or ` +
        `props of "${this.constructor.displayName}". ` +
        `Either wrap the root component in a <Provider>, ` +
        `or explicitly pass "store" as a prop to "${this.constructor.displayName}".`
      );

      this.stateProps = computeStateProps(this.store);
      this.dispatchProps = computeDispatchProps(this.store);
      this.state = { props: this.computeNextState() };
    }

    recomputeStateProps() {
      const nextStateProps = computeStateProps(this.store);
      if (shallowEqual(nextStateProps, this.stateProps)) {
        return false;
      }

      this.stateProps = nextStateProps;
      return true;
    }

    recomputeDispatchProps() {
      const nextDispatchProps = computeDispatchProps(this.store);
      if (shallowEqual(nextDispatchProps, this.dispatchProps)) {
        return false;
      }

      this.dispatchProps = nextDispatchProps;
      return true;
    }

    computeNextState(props = this.props) {
      return computeNextState(
        this.stateProps,
        this.dispatchProps,
        props
      );
    }

    recomputeState(props = this.props) {
      const nextState = this.computeNextState(props);
      if (!shallowEqual(nextState, this.state.props)) {
        this.setState({ props: nextState });
      }
    }

    isSubscribed() {
      return typeof this.unsubscribe === 'function';
    }

    trySubscribe() {
      if (shouldSubscribe && !this.unsubscribe) {
        this.unsubscribe = this.store.subscribe(::this.handleChange);
        this.handleChange();
      }
    }

    tryUnsubscribe() {
      if (this.unsubscribe) {
        this.unsubscribe();
        this.unsubscribe = null;
      }
    }

    componentDidMount() {
      this.trySubscribe();
    }

    componentWillReceiveProps(nextProps) {
      if (!shallowEqual(nextProps, this.props)) {
        this.recomputeState(nextProps);
      }
    }

    componentWillUnmount() {
      this.tryUnsubscribe();
    }

    handleChange() {
      if (this.recomputeStateProps()) {
        this.recomputeState();
      }
    }

    getWrappedInstance() {
      return this.refs.wrappedInstance;
    }

    render() {
      return (
        <WrappedComponent ref='wrappedInstance' {...this.state.props} />
      );
    }
  }

  if ((
    // Node-like CommonJS environments (Browserify, Webpack)
    typeof process !== 'undefined' &&
    typeof process.env !== 'undefined' &&
    process.env.NODE_ENV !== 'production'
   ) ||
    // React Native
    typeof __DEV__ !== 'undefined' &&
    __DEV__ //eslint-disable-line no-undef
  ) {
    Provide.prototype.componentWillUpdate = function componentWillUpdate () {
      if (this.version === version) {
        return;
      }

      // We are hot reloading!
      this.version = version;

      // Update the state and bindings.
      this.trySubscribe();
      this.recomputeStateProps();
      this.recomputeDispatchProps();
      this.recomputeState();
    };
  }

  return Provide;
}

Une chose à mentionner à propos du décorateur provide est que si vous regardez tout en utilisant react-devtools , vous verrez quelque chose comme ça ( Branches est enveloppé dans ProvideBranches(theme,packageList,sources) ):
2015-08-15-010648_1366x768_scrot

Au lieu de cela (que vous verriez avec l'original connect , où Branches est enveloppé avec Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Limites

J'ai seulement commencé à apprendre environ redux deux jours, donc je n'ai honnêtement aucune idée des limitations (le cas échéant) résultant de cette approche. Du haut de ma tête, je ne peux pas vraiment penser à aucun, et tout semble fonctionner parfaitement pour mes cas d'utilisation, mais peut-être que quelqu'un de plus expérimenté peut intervenir.

J'ai proposé cette approche particulière parce que j'aime vraiment l'idée d'avoir des composants _vraiment_ " stupides " qui peuvent avoir n'importe quel fournisseur qui leur est assigné. Il applique une véritable séparation des préoccupations et permet un nombre illimité de fournisseurs facilement interchangeables en tant que modules.

De plus, si @gaearon aime cette approche et pense qu'elle entre dans le cadre de react-redux , je serais heureux de soumettre un PR avec les ajouts. Sinon, je vais probablement créer et publier react-redux-providers , puis éventuellement publier des exemples de fournisseurs (par exemple, des trucs comme react-redux-provide-toggle ) avec quelques exemples du monde réel.

Parce que vous pouvez éviter les re-rendus inutiles des composants de niveau intermédiaire ?

Oui

La direction que nous avons prise maintenant est que nous allons utiliser Immutable.js pour notre état (à la fois App State et UI State ; nous gardons un certain UI State dans les composants), puis utiliser le PureRenderMixin pour tous nos composants. Cela n'éliminerait-il pas la pénalité de performance liée à l'utilisation du flux descendant ?

Edit: je viens de réaliser que cela n'éliminera pas cette pénalité de performance pour les composants de niveau intermédiaire si l'un de leurs composants de niveau inférieur doit encore être restitué, mais cela devrait toujours éliminer une grande partie de la surcharge. Une expérience avec ça ?

@ danmaz74 Nous avons rencontré des problèmes de performances dans des scénarios comme celui-ci (en n'utilisant pas de redux, mais une

@eldh merci pour la mise à jour, c'est logique. Nous garderons cela à l'esprit tout en continuant :)

Des mises à jour à ce sujet ? l'exemple de timbur est parfaitement logique pour moi alors que la pratique de connexion unique que je ne comprends pas très bien. Je serais intéressé par le contre-argument, à savoir « les gens ont des préférences différentes ici ».

En utilisant une seule connexion, il faudrait une énorme fonction mapStateToProps pour transformer l'état jusqu'à, par exemple, une hiérarchie profonde de 10 composants... Je suis un peu confus quant à l'idée derrière cela ou si je comprends mal quelque chose...

En utilisant une seule connexion, il faudrait une énorme fonction mapStateToProps

Personne ne préconise une connexion unique.

Ensuite, nous enveloppons les composants que nous voulons connecter à Redux avec la fonction connect() de react-redux. Essayez de ne le faire que pour un composant de niveau supérieur ou des gestionnaires de routage . Bien que techniquement vous puissiez connecter () n'importe quel composant de votre application au magasin Redux, évitez de le faire trop profondément car cela rendra le flux de données plus difficile à tracer.

Le "simple" ne fait référence qu'aux petites applications comme celle que nous créons dans l'exemple. N'hésitez pas à modifier les documents pour mieux clarifier cela. Je suis maintenant occupé avec d'autres projets, alors ne vous attendez pas à ce que ce problème bouge à moins que quelqu'un ne fasse un PR. Tu peux le faire aussi.

Merci pour la clarification.

Enfin, j'ai sorti react-redux-provide . Vérifiez-le ici . Je publierai également une poignée d'autres choses dans les prochains jours/semaines.

Ça a l'air très bien, merci !

Je viens de démarrer un nouveau projet de redux et j'utilise connect pour des composants "intelligents" - cela a beaucoup plus de sens pour moi, et s'il y a un avantage en termes de performances, il y a une victoire supplémentaire. Inversement, si vous avez transféré tout le contrôle jusqu'à l'application principale ou aux routeurs, il y a une perte complète de SRP - quelle taille votre application doit-elle atteindre avant de commencer à décomposer les choses ?

Je pense même à organiser les éléments associés dans un dossier de composants - c'est-à-dire. mettre un réducteur à côté de son composant principal, etc.

En fin de compte, je pense que le redux/flux est un énorme avantage pour un état prévisible, mais un tel changement mental par rapport au mv standard, quoi qu'il en soit, qui a rendu le développement d'applications d'interface utilisateur simple et accessible à tous, ce flux sera finalement supprimé et nous passerons retour à quelque chose qui ressemble plus à mv*.

Ceci est corrigé dans #1285.

Nous ne décourageons plus la création de composants de conteneur dans les documents mis à jour.
http://redux.js.org/docs/basics/UsageWithReact.html

Hé, j'ai écrit sur des trucs qui pourraient aider ici. :)

https://medium.com/@timbur/react -automatic-redux-providers-and-replicators-c4e35a39f1

Je pense que https://github.com/reactjs/redux/issues/419#issuecomment -183769392 peut également aider avec #1353.

@timbur Super article ! Pourriez-vous également partager vos réflexions sur cette question : https://github.com/reactjs/react-redux/issues/278

Je ne sais pas si cela est aveuglément évident, mais je pense que cela vaut la peine de l'ajouter ici pour plus de clarté, car je pense que beaucoup de ce qui a été dit est peut-être un peu abstrait pour quelqu'un qui découvre ce fil.

Lorsque j'ai commencé à utiliser Redux pour la première fois, j'ai par erreur pointillé des "conteneurs" (composants connectés) à l'intérieur (tout simplement de vieux composants stupides) parce que je pensais que mon application n'allait pas nécessiter beaucoup de découplage. Comme j'avais tort. Quand j'ai réalisé que j'avais besoin de réutiliser bon nombre de ces composants, j'ai dû faire pas mal de refactorisation et déplacer beaucoup de choses intelligentes jusqu'en haut de l'arborescence, mais cela est vite devenu difficile à manier ; un "fournisseur" en haut fournissant le contexte (comme il se doit) mais aussi fondamentalement TOUTES les applications (ce qu'il ne devrait pas faire).

La façon dont j'ai trouvé la meilleure approche est d'avoir une hiérarchie de conteneurs qui composent des composants stupides, avec un fournisseur au sommet. Les conteneurs ne devraient vivre qu'à l'intérieur d'autres conteneurs . Votre application doit être une hiérarchie de conteneurs qui utilisent des composants pour présenter leurs données.

Souvent, un bon moyen de le faire avec des listes consiste à transmettre des identifiants via des composants intelligents. Le besoin d'un identifiant est un signe que quelque chose appartient au domaine de l'application. Ainsi, dans la mesure du possible, prenez des listes d'identifiants dans un conteneur et transmettez-les à un autre conteneur qui peut utiliser les identifiants pour obtenir les informations souhaitées. Dans chaque conteneur, utilisez un composant pour restituer ces informations sans avoir besoin de l'ID.

Ci-dessous, j'ai simulé un exemple (alambiqué) de la façon de transmettre les parties connectées de l'application via une hiérarchie de conteneurs qui utilise des composants pour les afficher.

// Provider component that renders some containers and some components and provides the store
class TodoAppProvider {
  constructor() {
    // setup store etc.
  }

  render() {
    return (
      <Provider store={this.store}> {/* Provider from 'react-redux' */}
        <AppLayoutComponent title="My Todos" footer={<TodoFooter />}>
          <TodoListsContainer />
        </AppLayoutComponent>
      </Provider>
    );
  }
);

// AppLayoutComponent
// Lots of nice css, other dumb components etc. no containers!
export default const AppLayoutComponent = ({ title, children, footer }) => (
  <header>
    {title}
  </header>
  <main>
    {children /* This variable can be a container or components but it's not hardcoded! */}
  </main>
  <footer>
    {footer}
  </footer>
);

// TodoFooter
// Another dumb component
export default const TodoFooter = () => (
  <footer>
    &copy; {Date.now() /* we are copyrighted to the millisecond */}
  </footer>
);

// TodoListsContainer
// Smart component that renders all the lists
class TodoListsContainer extends React.Component {
  render() {
    return () {
      <div>
        {todoLists.map(id => (
          {/* this container renders another container */ }
          <TodoListContainer key={id} todoListId={id} />
        ))}
      </div>
    }
  }
}

const mapStateToProps = state => ({
  todoLists: getTodoLists(state),
});

export default connect(mapStateToProps)(TodoListsContainer);

// TodoListContainer
// Gets the props and visibleTodo IDs for the list
class TodoListContainer {
  render() {
    const { id, title, visibleTodos } = this.props;
    return (
      <div>
        {/* Render a component but passes any connected data in as props / children */}
        <TodoListPanelComponent title={title}>
          {visibleTodos.map(id => (
            <TodoContainer todoId={id} />
          ))}
        </TodoListPanelComponent>
      </div>
    );
  }
}

const mapStateToProps = (state, { todoListId }) => ({
  ...getTodoList(state, todoListId), // A todo object (assume we need all the attributes)
  visibleTodos: getVisibleTodos(state, todoListId), // returns ids
});

export default connect(mapStateToProps)(TodoListContainer);


// TodoListPanelComponent
// render the panel to sit the todos in
// children should be todos
// No containers!
export default const TodoListPanelComponent = ({ title, children }) => (
  <div>
    <h3>{title}</h3>
    <div>
      {children}
    </div>
  </div>
);

// TodoContainer
// This just wraps the TodoComponent and passed the props
// No separate class or JSX required!
const mapStateToProps = (state, { todoId }) => ({
  ...getTodo(state, todoId),
});

const mapDispatchToProps = (dispatch, { todoListId }) => ({
  handleFilter: () => dispatch(hideTodo(id)), // Pass ALL smart stuff in
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoComponent); // Passing in the component to connect

// TodoComponent
// Render the component nicely; again, as all of its connected stuff passed in
// The FilterLinkContainer is an example of a smell that will come back to bite you!
export default const TodoComponent = ({ content, isComplete, handleFilter }) => (
  <div>
    <div>
      {content}
    </div>
    <div>
      {isComplete ? '✓' : '✗'}
    </div>
    <div>
      {/* Don't do this, can't re-use TodoComponent outside the app context! */}
      <FilterLinkContainer />

      {/* Instead do this (or similar), component can be reused! */}
      <Link onClick={handleFilter}>
        'Filter'
      </Link>
    </div>
  </div>
);

Donc ici, la hiérarchie des conteneurs est TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Ils sont chacun rendus les uns par les autres, jamais rendus à l'intérieur d'un composant et ne contiennent aucun code de vue brut (à part les div pour des raisons d'emballage React).

En fin de compte, la façon dont j'aime y penser est comme si vous deviez initialement créer une arborescence de composants connectés qui mappent vos données de manière utile dont votre interface utilisateur se souciera. Cependant, il n'y a aucune interface utilisateur, juste un arbre d'état mappé à travers une hiérarchie de conteneurs uniquement . Après cela, vous parcourez et saupoudrez des composants de présentation à l'intérieur de ces conteneurs pour afficher les données de la manière que vous souhaitez (c'est-à-dire dans le DOM). Évidemment, il n'est pas utile d'écrire votre application de cette manière en deux étapes, mais j'ai trouvé utile de conceptualiser le modèle comme ceci.

Y a-t-il une pénalité de performance (ou une autre raison architecturale) pour l'utilisation de connect() plus d'une fois ? J'essaie de fournir des accessoires souvent utilisés aux composants en faisant abstraction de leur point d'entrée de connexion de la manière suivante :

// connectCommonProps.js (mergeProps not included for the sake of simplicity)

const _mapStateToProps = (state) => ({ [often used slices of state] });

const _mapDispatchToProps = (dispatch) => ({ [often used actions] });

const connectCommonProps = (mapStateToProps, mapDispatchToProps, component) => {
    // First connect
    const connectedComponent = connect(mapStateToProps, mapDispatchToProps)(component);

    // Second connect
    return connect(_mapStateToProps, _mapDispatchToProps)(connectedComponent);
};

export default connectMapAndFieldProps;
// Some component that needs the often used props
...
export default connectCommonProps(..., ..., Component);

Je suis paresseux ici et je n'ai pas combiné les deux versions de mapStateToProps et les deux versions de mapDispatchToProps car cela simplifie la déclaration. Mais je me demande si c'est une mauvaise idée de laisser connect faire ça pour moi.

@timotgl : Dan n'est plus un mainteneur actif - veuillez ne pas lui envoyer de ping directement.

J'étais sur le point de dire que votre question est répondue dans l'entrée Redux FAQ sur la connexion de plusieurs composants , mais il semble que vous posiez une question différente - enrouler délibérément plusieurs connexions autour d'un seul composant? Je ne peux pas dire que j'ai déjà vu quelqu'un faire ça auparavant, et j'ai vu un _lot_ de code Redux.

Personnellement, je suggérerais d'essayer une approche différente. Soit avoir un HOC "props communs" ou quelque chose qui ne fait que les transmettre à ses enfants, ou utiliser des sélecteurs pour récupérer les props communs dans la fonction mapState du composant spécifique et les combiner avec les props spécifiques nécessaires.

@markerikson Désolé, je ne le savais pas, la mention at a été supprimée.

Donc tout d'abord, cela fonctionne et le composant apparaît comme n'importe quel autre composant connecté dans les outils de développement de réaction, il n'a pas de wrapper supplémentaire ou quelque chose comme ça.

J'ai décidé contre un HOC parce que je ne voulais pas impliquer le paradigme POO/héritage, puisqu'il s'agit simplement de fournir des accessoires supplémentaires au composant, son comportement est autrement inchangé.

Bon point pour faire le câblage dans mapStateToProps . Cela fonctionnerait, mais j'ai au moins 2 points d'entrée - appeler une fonction d'assistance pour se connecter semble plus simple.

Je ne sais pas ce que vous entendez par "deux points d'entrée".

Ce que j'imagine, c'est quelque chose comme ça :

import {selectCommonProps} from "app/commonSelectors";

import {selectA, selectB} from "./specificSelectors";

const mapState = (state) => {
    const propA = selectA(state);
    const propB = selectB(state);
    const commonProps = selectCommonProps(state);

    return {a, b, ...commonProps};
}

@markerikson Par deux points d'entrée, je voulais dire que vous deviez faire la même chose pour mapDispatchToProps , et potentiellement pour mergeProps .

Vous ne devriez presque jamais utiliser mergeProps - c'est là comme une trappe d'évacuation de dernier recours, et nous déconseillons son utilisation. Je vous recommande également généralement de ne pas écrire une vraie fonction mapDispatch et d'utiliser à la place le "abréviation d'objet":

import {addTodo, toggleTodo} from "./todoActions";

class TodoList extends Component {}

const actions = {addTodo, toggleTodo};
export default connect(mapState, actions)(TodoList);

Vous pouvez facilement avoir un fichier index.js qui réexporte tous vos créateurs d'actions "communs", et faire quelque chose comme :

import * as commonActions from "app/common/commonActions";
import {specificAction1, specificAction2} from "./actions";

const actionCreators = {specificAction1, specificAction2, ...commonActions};

export default connect(null, actionCreators)(MyComponent);

Vous ne devriez presque jamais utiliser mergeProps - c'est là comme une trappe d'évacuation de dernier recours, et nous déconseillons son utilisation.

Bonjour @markerikson , je suis juste curieux de savoir pourquoi quelqu'un devrait éviter d'utiliser mergeProps ? Je trouve très pratique de "cacher" les accessoires de mapStateToProps dont je pourrais avoir besoin dans mes actions dans mapDispatchToProps mais pas dans le composant. Est-ce une mauvaise chose ?

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