Redux: Empfohlene Verwendung von connect()

Erstellt am 7. Aug. 2015  ·  34Kommentare  ·  Quelle: reduxjs/redux

Hallo @gaearon - die folgende Aussage hat mich beim Lesen der Dokumente

Dann wickeln wir die Komponenten, die wir mit Redux verbinden möchten, mit der Funktion connect() von React-Redux ein. Versuchen Sie, dies nur für eine Komponente der obersten Ebene oder für Routing-Handler zu tun. Obwohl Sie technisch gesehen jede Komponente in Ihrer App mit dem Redux Store verbinden können, vermeiden Sie es, dies zu tief zu tun, da dies die Rückverfolgung des Datenflusses erschwert.

Tiefe Requisitenketten waren eines der Probleme mit React, die mich dazu veranlassten, Flux zu verwenden. Der Kompromiss für die Vereinfachung der Nachverfolgung des Datenflusses besteht darin, dass Sie den Prop-Flow einrichten/pflegen/nachverfolgen müssen, und was noch wichtiger ist, dass Eltern über die Datenanforderungen ihrer Kinder Bescheid wissen müssen. Aus meiner Erfahrung bin ich nicht davon überzeugt, dass dieser Ansatz besser ist, aber ich bin gespannt, was Sie denken :smile:

docs

Hilfreichster Kommentar

Gibt es andere Probleme als die Verfolgung des Datenflusses beim Verbinden () vieler Komponenten? Würde es zum Beispiel eine Leistungseinbuße geben?

Nein, das Gegenteil ist der Fall: Sie erzielen eine bessere Leistung, indem Sie auf den Top-Down-Flow verzichten.

Alle 34 Kommentare

Ich stimme Brent voll und ganz zu. Dies war ein großer Teil der grundlegenden Denkweise hinter so etwas wie Relay und führt zu einem höheren Zusammenhalt.

Vielleicht sollten wir einfach sagen „die Leute haben hier unterschiedliche Präferenzen“.

@gaearon - vielleicht wäre ein Abschnitt über die Kompromisse nützlich, der eine Demonstration enthält, wie die Verfolgung des Datenflusses erschwert wird. Da könnte ich auf meiner Seite die Präferenzen einbringen.

Cool. Lassen Sie es uns offen halten und nach der Beruhigung der ersten Dokumente erneut besuchen.

Klingt gut @gaearon! :) Ping mich an, wenn du wiederkommen möchtest

Ich bin ziemlich neu bei Redux, aber das war im letzten Jahr ein ziemlich großer Schmerzpunkt für mich, eine App zu entwickeln, bei der wir die Datenstruktur ziemlich stark verändert haben. Ich muss also sagen, dass ich @brentvatne in diesem

Gibt es andere Probleme als die Verfolgung des Datenflusses beim Verbinden () vieler Komponenten? Würde es zum Beispiel eine Leistungseinbuße geben?

Gibt es andere Probleme als die Verfolgung des Datenflusses beim Verbinden () vieler Komponenten? Würde es zum Beispiel eine Leistungseinbuße geben?

Nein, das Gegenteil ist der Fall: Sie erzielen eine bessere Leistung, indem Sie auf den Top-Down-Flow verzichten.

Weil Sie unnötige Re-Renderings von Komponenten der mittleren Ebene vermeiden können?

Jawohl.

Um die Diskussion zu ergänzen: Ich beschränke hauptsächlich meine Verwendung von Connect-to-Routing-Komponenten. Wenn ich eine Seite habe, die eine zusätzliche intelligente Komponente verwenden könnte (z. B. ein Formular-Modal), bestand meine Problemumgehung darin, das Element oder den Knoten zu übergeben und die dumme Komponente rendern zu lassen. Es bedeutet, dass Sie etwas mehr Boilerplate haben, aber das Testen der dummen Komponente ist immer noch einfach. Ich experimentiere noch damit, aber ich denke, dies könnte der beste Weg sein, um intelligente Komponenten zusammenzustellen, ohne auf die einfache Testbarkeit zu verzichten.

Um ein grobes Beispiel zu geben:

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 Wenn Sie etwas schreiben

Update vom 19.10.2015: Ich habe diesen Kommentar verbessert und als react-redux-provide . Siehe https://github.com/rackt/redux/issues/419#issuecomment -149325401 unten.

Fortsetzung von #475, werde ich beschreiben, was ich mir ausgedacht habe, obwohl diese Diskussion jetzt wahrscheinlich in react-redux . ;)

Modulare Anbieter mit seitlicher Zuweisung

Um es kurz zu machen, der Ansatz, den ich gewählt habe, dreht sich um modulare Anbieter, die Sie jeder Komponente zuordnen können. Dies ermöglicht _wirklich_ "dumme" Komponenten, erzwingt eine maximale Trennung von Belangen und ermöglicht die sehr einfache und schnelle Nutzung und gemeinsame Nutzung beliebig vieler austauschbarer Anbieter. Es erzwingt auch eine effizientere Methode zum Aktualisieren von Komponenten.

Also... Ich habe die Verzeichnisse actions , constants , containers , reducers und stores (in den Beispielen üblich) ) und ersetzte sie durch ein einzelnes providers Verzeichnis. Alternativ ist möglicherweise nicht einmal ein Verzeichnis providers erforderlich, da dieser Ansatz es ermöglichen würde, eigenständige Anbieter zu paketieren und zu verteilen. Ich denke, wir werden sehen, dass einige wirklich coole Sachen auftauchen, wenn dieser spezielle Ansatz übernommen wird!! (Anmerkung: Es ist natürlich nicht notwendig, diese Verzeichnisse in einem zu konsolidieren, aber ich denke, es 1) macht die Dinge viel einfacher zu lesen / zu verstehen und 2) reduziert Boilerplate und 3) einzelne Anbieter sind klein genug, dass es Sinn macht. )

Beispiel "Dumme" Komponente

// 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>
    );
  }
}

Beispielanbieter

// 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;

Seitwärtszuordnung

Die Idee ist, wie erwähnt, einfach beliebige Provider „dummen“ Komponenten zuordnen zu können. Wenn Sie Ihre App mounten, können Sie dies wie folgt tun:

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')
);

Hoffentlich ist das alles ziemlich einfach, aber ich gehe gerne näher darauf ein und füge bei Bedarf Kommentare zur Klarstellung hinzu.

Zusatzcode

Sie werden wahrscheinlich feststellen, dass in diesen Beispielen eine Handvoll Hilfsfunktionen importiert werden. Dies sind winzige Module (3 der 4 sind nur wenige Zeilen lang), die im Grunde nur eine Kombination aus existierenden Methoden redux und react-redux sind, die für die gängigsten Anwendungsfälle entwickelt wurden. Natürlich sind Sie nicht auf diese Funktionen beschränkt. Sie existieren nur, um die Dinge noch einfacher zu machen.

// 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);
      }
    }
  }
}

Und zu guter Letzt haben wir den provide Dekorateur. Es ist eine modifizierte Version von connect entwickelt wurde, um eine seitliche Zuordnung zu ermöglichen.

// 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;
}

Eine erwähnenswerte Sache über den provide Dekorator ist, dass, wenn Sie sich alles mit react-devtools ansehen, Sie so etwas sehen ( Branches wird in ProvideBranches(theme,packageList,sources) verpackt
2015-08-15-010648_1366x768_scrot

Stattdessen (was Sie mit dem ursprünglichen connect , wobei Branches mit Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Einschränkungen

Ich habe erst vor zwei Tagen angefangen, etwas über redux zu lernen, also habe ich ehrlich gesagt keine Ahnung, welche (wenn überhaupt) Einschränkungen aufgrund dieses Ansatzes bestehen. Auf den ersten Blick fallen mir keine ein, und alles scheint für meine Anwendungsfälle perfekt zu funktionieren, aber vielleicht kann sich jemand mit mehr Erfahrung einmischen.

Ich bin auf diesen speziellen Ansatz gekommen, weil ich die Idee wirklich mag, _wirklich_ "dumme" Komponenten zu haben, denen jeder Provider zugewiesen werden kann. Es erzwingt eine echte Trennung der Belange und ermöglicht eine beliebige Anzahl von leicht austauschbaren Anbietern als Module.

Wenn @gaearon diesen Ansatz mag und der Meinung ist, dass er in den Bereich von react-redux fällt, würde ich gerne eine PR mit den Ergänzungen einreichen. Wenn nicht, werde ich wahrscheinlich react-redux-providers erstellen und veröffentlichen und schließlich Beispielanbieter (zB Sachen wie react-redux-provide-toggle ) zusammen mit einigen Beispielen aus der Praxis veröffentlichen.

Weil Sie unnötige Re-Renderings von Komponenten der mittleren Ebene vermeiden können?

Jawohl

Die Richtung, die wir jetzt eingeschlagen haben, ist, dass wir Immutable.js für unseren Status (sowohl App State als auch UI State; wir behalten einen gewissen UI State in den Komponenten) verwenden und dann PureRenderMixin für alle unsere Komponenten verwenden. Würde dies nicht die Leistungseinbußen bei der Verwendung des Top-Down-Flows beseitigen?

Bearbeiten: Mir ist gerade klar geworden, dass es diese Leistungseinbußen für Komponenten mittlerer Ebene nicht beseitigen wird, wenn eine ihrer Komponenten auf niedrigerer Ebene noch neu gerendert werden muss, aber es sollte immer noch einen Großteil des Overheads eliminieren. Irgendwelche Erfahrungen damit?

@ danmaz74 Wir sind in gestoßen (nicht mit Redux, sondern mit einer sehr ähnlichen hausgemachten Lib). Wir haben jedoch eine ziemlich komplexe App mit einigen sehr "teuren" Komponenten. Wenn eine App größer wird, kann das Einfügen von Daten an mehr Stellen als nur auf der obersten Ebene dazu beitragen, implizite Abhängigkeiten zwischen Komponenten zu vermeiden und zu vermeiden, dass Eltern zu viel über die Datenanforderungen ihrer Kinder wissen.

@eldh danke für das Update, das macht Sinn. Wir werden es im Hinterkopf behalten, während wir weitermachen :)

Irgendwelche Updates dazu? Timburs Beispiel macht für mich absolut Sinn, während ich die Single-Connect-Praxis nicht ganz verstehe. Mich würde das Gegenargument interessieren, nämlich „die Leute haben hier unterschiedliche Präferenzen“.

Mit einem einzigen Connect würde man eine riesige mapStateToProps-Funktion benötigen, um den Zustand bis hin zu einer tiefen Hierarchie mit 10 Komponenten zu transformieren ... Ich bin etwas verwirrt, was der Gedanke dahinter ist oder ob ich etwas falsch verstehe ...

Mit einer einzigen Verbindung würde man eine riesige mapStateToProps-Funktion benötigen

Niemand befürwortet eine einzige Verbindung.

Dann wickeln wir die Komponenten, die wir mit Redux verbinden möchten, mit der Funktion connect() von React-Redux ein. Versuchen Sie, dies nur für eine Komponente der obersten Ebene oder für Routing-Handler zu tun. Obwohl Sie technisch gesehen jede Komponente in Ihrer App mit dem Redux Store verbinden können (), vermeiden Sie es, dies zu tief zu tun, da dies die Rückverfolgung des Datenflusses erschwert.

Das "Single" bezieht sich nur auf kleine Apps wie die, die wir im Beispiel erstellen. Bitte zögern Sie nicht, die Dokumente zu ändern, um dies besser zu verdeutlichen. Ich bin jetzt mit anderen Projekten beschäftigt, also erwarte bitte nicht, dass dieses Thema in Bewegung kommt, es sei denn, jemand macht eine PR. Du kannst es auch tun.

Danke für die Abklärung.

Endlich bin ich dazu gekommen, react-redux-provide freizugeben. Schau es dir hier an . Ich werde in den nächsten Tagen/Wochen auch noch eine Handvoll anderer Sachen veröffentlichen.

Das sieht sehr gut aus, danke!

Ich habe gerade ein neues Redux-Projekt gestartet und verwende Connect für "intelligente" Komponenten - das macht für mich viel mehr Sinn, und wenn es einen Perf-Vorteil gibt, gibt es einen zusätzlichen Gewinn. Umgekehrt, wenn Sie die gesamte Kontrolle auf die Haupt-App oder die Router übertragen, gibt es einen vollständigen Verlust von SRP - wie groß muss Ihre App werden, bevor Sie anfangen, die Dinge zu zerstören?

Ich denke sogar darüber nach, verwandte Elemente in einem Komponentenordner zu organisieren - dh. Setzen Sie ein Reduzierstück neben die Hauptkomponente usw.

Letztendlich glaube ich, dass Redux/Flux ein großer Vorteil für den vorhersehbaren Zustand ist, aber eine solche mentale Verschiebung von Standard-MV-was auch immer die Entwicklung von UI-Apps einfach und für jeden zugänglich gemacht hat, dass Flux irgendwann abstrahiert wird und wir uns bewegen werden zurück zu etwas, das eher wie mv* aussieht.

Dies wird in #1285 behoben.

Wir raten nicht mehr davon ab, Containerkomponenten in den aktualisierten Dokumenten zu erstellen.
http://redux.js.org/docs/basics/UsageWithReact.html

Hey, ich habe über einige Dinge geschrieben, die hier helfen könnten. :)

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

Ich denke, https://github.com/reactjs/redux/issues/419#issuecomment -183769392 kann auch bei #1353 helfen.

@timbur Toller Artikel! Könnten Sie bitte auch Ihre Gedanken zu dieser Frage mitteilen :

Ich bin mir nicht sicher, ob dies blendend offensichtlich ist, aber ich denke, es lohnt sich, es hier aus Gründen der Klarheit hinzuzufügen, da ich denke, dass vieles von dem, was gesagt wurde, vielleicht etwas abstrakt ist für jemanden, der neu in diesen Thread kommt.

Als ich anfing, Redux zu verwenden, habe ich fälschlicherweise "Container" (verbundene Komponenten) in (einfach nur alte dumme) "Komponenten" gepunktet, weil ich dachte, dass meine App nicht viel Entkopplung erfordern würde. Wie falsch ich lag. Als mir klar wurde, dass ich viele dieser Komponenten wiederverwenden musste, musste ich einiges an Refactoring durchführen und viele intelligente Dinge ganz nach oben verschieben, aber das wurde bald unhandlich; ein "Anbieter" oben, der den Kontext bereitstellt (wie er sollte), aber auch im Grunde die gesamte App (was nicht der Fall sein sollte).

Die beste Herangehensweise ist meiner Meinung nach eine Hierarchie von Containern, die dumme Komponenten bilden, mit einem Anbieter an der Spitze. Container sollten nur in anderen Containern leben . Ihre App sollte eine Hierarchie von Containern sein, die Komponenten verwenden, um ihre Daten darzustellen.

Häufig ist dies bei Listen eine gute Möglichkeit, IDs durch intelligente Komponenten weiterzugeben. Die Notwendigkeit einer ID ist ein Zeichen dafür, dass etwas zur Domäne der App gehört. Nehmen Sie also nach Möglichkeit ID-Listen in einem Container und geben Sie sie an einen anderen Container weiter, der die IDs verwenden kann, um die gewünschten Informationen zu erhalten. Verwenden Sie in jedem Container eine Komponente, um diese Informationen ohne die Notwendigkeit der ID zu rendern.

Im Folgenden habe ich ein (verschachteltes) Beispiel dafür dargestellt, wie die verbundenen Teile der App durch eine Containerhierarchie weitergegeben werden, die Komponenten verwendet, um sie anzuzeigen.

// 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>
);

Hier ist die Containerhierarchie also TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Sie werden jeweils von einander gerendert, niemals innerhalb einer Komponente gerendert und enthalten keinen Rohansichtscode (abgesehen von gelegentlichen div aus React-Wrapping-Gründen).

Letztendlich stelle ich mir das so vor, als ob Sie zunächst einen Baum aus verbundenen Komponenten erstellen würden, der Ihre Daten auf eine nützliche Weise abbildet, die Ihre Benutzeroberfläche _will_ interessiert. Es gibt jedoch überhaupt keine Benutzeroberfläche, sondern nur einen Zustandsbaum, der durch eine Hierarchie von nur Containern abgebildet wird. Danach gehen Sie durch und verteilen Präsentationskomponenten in diesen Containern, um die Daten tatsächlich auf beliebige Weise anzuzeigen (dh im DOM). Offensichtlich ist es nicht sinnvoll, Ihre App auf diese Weise in zwei Durchgängen zu schreiben, aber ich fand es nützlich, das Modell so zu konzipieren.

Gibt es eine Leistungseinbuße (oder einen anderen architektonischen Grund), wenn connect() mehr als einmal verwendet wird? Ich versuche, Komponenten häufig verwendete Requisiten bereitzustellen, indem ich ihren Connect-Einstiegspunkt wie folgt abstrahiere:

// 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);

Ich bin hier faul und habe die beiden Versionen von mapStateToProps und die beiden Versionen von mapDispatchToProps nicht kombiniert, da dies die Deklaration einfach hält. Aber ich frage mich, ob es eine schlechte Idee ist, connect diese Arbeit für mich erledigen zu lassen.

@timotgl : Dan ist kein aktiver Betreuer mehr - bitte

Ich wollte sagen, dass Ihre Frage im Redux-FAQ-Eintrag zum Verbinden mehrerer Komponenten beantwortet wird, aber es sieht so aus, als ob Sie nach etwas anderem fragen - absichtlich mehrere Verbindungen um eine einzelne Komponente wickeln? Ich kann nicht sagen, dass ich jemals jemanden gesehen habe, der das getan hat, und ich habe eine Menge Redux-Code gesehen.

Persönlich würde ich vorschlagen, einen anderen Ansatz zu versuchen. Entweder haben Sie ein HOC "gemeinsame Requisiten" oder etwas, das diese meistens nur an seine Kinder weitergibt, oder verwenden Sie Selektoren, um die gemeinsamen Requisiten in der Funktion mapState der spezifischen Komponente abzurufen und diese mit den spezifischen benötigten Requisiten zu kombinieren.

@markerikson Das wusste

Zunächst einmal funktioniert es, und die Komponente erscheint wie jede andere verbundene Komponente in den Reaktions-Entwicklungstools, sie hat keinen zusätzlichen Wrapper oder ähnliches.

Ich habe mich gegen einen HOC entschieden, weil ich das OOP/Vererbungs-Paradigma nicht einbeziehen wollte, da es nur darum geht, der Komponente einige weitere Requisiten zur Verfügung zu stellen, ihr Verhalten ist ansonsten unberührt.

Guter Punkt bei der Verkabelung in mapStateToProps . Das würde funktionieren, aber dann habe ich mindestens 2 Einstiegspunkte - das Aufrufen einer Hilfsfunktion zum Verbinden scheint einfacher.

Ich bin mir nicht sicher, was Sie mit "zwei Einstiegspunkten" meinen.

Was ich mir vorstelle ist ungefähr so:

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 Mit zwei Einstiegspunkten meinte ich, dass Sie dasselbe für mapDispatchToProps und möglicherweise für mergeProps tun müssen.

Sie sollten mergeProps fast nie verwenden - es dient als letztes Mittel zur Flucht, und wir raten davon ab. Ich empfehle auch im Allgemeinen, dass Sie keine echte mapDispatch Funktion schreiben und stattdessen die "Objektkürzel" verwenden:

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

class TodoList extends Component {}

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

Sie könnten leicht eine index.js-Datei haben, die alle Ihre "gemeinsamen" Aktionsersteller reexportiert, und so etwas tun:

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

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

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

Sie sollten mergeProps fast nie verwenden - es dient als letztes Mittel zur Flucht, und wir raten davon ab.

Hallo @markerikson , ich bin nur neugierig, warum jemand die Verwendung von mergeProps vermeiden sollte? Ich finde es sehr praktisch, Requisiten aus den mapStateToProps zu "verstecken", die ich in meinen Aktionen in mapDispatchToProps, aber nicht in der Komponente benötigen könnte. Ist es eine schlechte Sache?

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen