Redux: Uso recomendado de connect ()

Creado en 7 ago. 2015  ·  34Comentarios  ·  Fuente: reduxjs/redux

Hola @gaearon : la siguiente declaración me tomó por sorpresa mientras leía los documentos:

Luego, empaquetamos los componentes que queremos conectar a Redux con la función connect () de react-redux. Intente hacer esto solo para un componente de nivel superior o controladores de ruta. Si bien técnicamente puede conectar () cualquier componente de su aplicación a la tienda Redux, evite hacer esto con demasiada profundidad porque hará que el flujo de datos sea más difícil de rastrear.

Las cadenas de accesorios profundos fue uno de los problemas con React que me llevó a usar Flux; La compensación para facilitar el seguimiento del flujo de datos es que debe configurar / mantener / rastrear el flujo de accesorios y, lo que es más importante, los padres deben conocer los requisitos de datos de sus hijos. Por mi experiencia, no estoy convencido de que este enfoque sea mejor, pero tengo curiosidad por escuchar lo que piensas: sonríe:

docs

Comentario más útil

¿Hay otros problemas además de rastrear el flujo de datos al conectar () muchos componentes? ¿Habría una penalización por desempeño, por ejemplo?

No, es exactamente lo contrario: se obtiene un mejor rendimiento renunciando al flujo de arriba hacia abajo.

Todos 34 comentarios

Estoy totalmente de acuerdo con Brent. Este fue gran parte del pensamiento fundamental detrás de algo como Relay y conduce a una mayor cohesión.

Quizás deberíamos decir simplemente "la gente tiene preferencias diferentes aquí".

@gaearon : quizás sea útil una sección sobre las compensaciones que incluya una demostración de cómo complica el seguimiento del flujo de datos. Podría contribuir a mi lado de las preferencias para eso.

Frio. Dejémoslo abierto y volvamos a visitarlo después de que se establezcan los documentos iniciales.

¡Suena bien @gaearon! :) Hazme ping cuando quieras volver a visitar

Soy bastante nuevo en Redux, pero este ha sido un gran problema para mí durante el último año, crear una aplicación en la que hemos cambiado bastante la estructura de datos. Así que tengo que decir que estoy realmente de acuerdo con @brentvatne en este caso.

¿Hay otros problemas además de rastrear el flujo de datos al conectar () muchos componentes? ¿Habría una penalización por desempeño, por ejemplo?

¿Hay otros problemas además de rastrear el flujo de datos al conectar () muchos componentes? ¿Habría una penalización por desempeño, por ejemplo?

No, es exactamente lo contrario: se obtiene un mejor rendimiento renunciando al flujo de arriba hacia abajo.

¿Porque puede evitar repeticiones innecesarias de componentes de nivel medio?

Si.

Para agregar a la discusión: principalmente estoy limitando mi uso de conectar para enrutar componentes. Cuando tengo una página que podría usar un componente inteligente adicional (por ejemplo, un formulario modal), mi solución ha sido pasar el elemento o nodo y hacer que el componente tonto lo represente. Significa que tiene un poco más de repetición, pero probar el componente tonto sigue siendo fácil. Todavía estoy experimentando con esto, pero creo que esta podría ser la mejor manera de componer componentes inteligentes sin renunciar a la facilidad de prueba.

Para dar un ejemplo aproximado:

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 tiene ganas de escribir algo, averigüe dónde encajarlo en la estructura actual del documento y no dude en seguir adelante. :-)

Actualización del 19/10/2015: he mejorado este comentario y lo he publicado como react-redux-provide . Consulte https://github.com/rackt/redux/issues/419#issuecomment -149325401 a continuación.

Continuando del # 475, describiré lo que se me ocurrió, aunque esta discusión probablemente ahora pertenezca a react-redux . ;)

Proveedores modulares que utilizan la asignación lateral

En pocas palabras, el enfoque que he adoptado gira en torno a proveedores modulares que puede asignar a cualquier componente. Esto permite componentes _verdaderamente_ "tontos", impone una separación máxima de preocupaciones y hace posible utilizar y compartir muy fácil y rápidamente cualquier número de proveedores intercambiables. También impone una forma más eficiente de actualizar componentes.

Entonces ... me deshice de los directorios actions , constants , containers , reducers y stores (comunes en los ejemplos ) y los reemplazó con un solo directorio providers . Alternativamente, un directorio providers puede que ni siquiera sea necesario, ya que este enfoque permitiría empaquetar y distribuir proveedores independientes. ¡Creo que veremos surgir algunas cosas realmente interesantes si se adopta este enfoque en particular! (Nota: por supuesto, no es necesario consolidar esos directorios en uno, pero creo que 1) hace las cosas mucho más fáciles de leer / entender y 2) reduce el texto estándar y 3) los proveedores individuales son lo suficientemente pequeños como para tener sentido. )

Ejemplo de componente "tonto"

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

Proveedor de ejemplo

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

Asignación lateral

Como se mencionó, la idea es poder asignar fácilmente proveedores arbitrarios a componentes "tontos". Entonces, cuando monte su aplicación, puede hacerlo así:

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

Con suerte, todo esto es bastante sencillo, pero me complacerá entrar en más detalles y agregar comentarios para aclarar si es necesario.

Código adicional

Probablemente notará que se importan un puñado de funciones de utilidad en estos ejemplos. Estos son módulos pequeños (3 de los 4 tienen solo unas pocas líneas) que son básicamente una combinación de los métodos redux y react-redux , diseñados para los casos de uso más comunes. Por supuesto, no está limitado a estas funciones. Solo existen para hacer las cosas aún más fáciles.

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

Y por último, pero no menos importante, tenemos el decorador provide . Es una versión modificada de connect diseñada para permitir la asignación lateral.

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

Una cosa que vale la pena mencionar sobre el decorador provide es que si miras todo usando react-devtools , verás algo como esto ( Branches se envuelve en ProvideBranches(theme,packageList,sources) ):
2015-08-15-010648_1366x768_scrot

En lugar de esto (que verías con el connect , donde Branches se envuelve con Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Limitaciones

Solo comencé a aprender sobre redux dos días, por lo que, honestamente, no tengo idea de las limitaciones (si las hay) que existen como resultado de este enfoque. En la parte superior de mi cabeza, realmente no puedo pensar en ninguno, y todo parece funcionar perfectamente para mis casos de uso, pero tal vez alguien más experimentado pueda intervenir.

Se me ocurrió este enfoque en particular porque realmente me gusta la idea de tener componentes _ realmente_ "tontos" que puedan tener cualquier proveedor asignado. Impone una verdadera separación de preocupaciones y permite cualquier número de proveedores fácilmente intercambiables como módulos.

Además, si a @gaearon le gusta este enfoque y cree que está dentro del alcance de react-redux , me complacería enviar un PR con las adiciones. Si no, probablemente crearé y publicaré react-redux-providers , y luego eventualmente publicaré ejemplos de proveedores (por ejemplo, cosas como react-redux-provide-toggle ) junto con algunos ejemplos del mundo real.

¿Porque puede evitar repeticiones innecesarias de componentes de nivel medio?

La dirección que hemos tomado ahora es que vamos a usar Immutable.js para nuestro estado (tanto App State como UI State; mantenemos algo de UI State en los componentes), y luego usaremos PureRenderMixin para todos nuestros componentes. ¿No eliminaría esto la penalización de rendimiento de usar el flujo de arriba hacia abajo?

Editar: Me acabo de dar cuenta de que no eliminará esa penalización de rendimiento para los componentes de nivel medio si uno de sus componentes de nivel inferior aún necesita volver a renderizarse, pero aún así debería eliminar gran parte de la sobrecarga. ¿Alguna experiencia con eso?

@ danmaz74 Hemos tenido problemas de biblioteca casera muy similar). Sin embargo, tenemos una aplicación bastante compleja, con algunos componentes muy "costosos". Además, a medida que una aplicación crece, inyectar datos en más lugares además del nivel superior puede ayudarlo a evitar la creación de dependencias implícitas entre componentes y evitar que los padres sepan demasiado sobre los requisitos de datos de sus hijos.

@eldh gracias por la actualización, eso tiene sentido. Lo tendremos en cuenta mientras continuamos :)

¿Alguna actualización sobre esto? El ejemplo de timbur tiene mucho sentido para mí, mientras que la práctica de conexión única no entiendo del todo. Me interesaría el argumento contrario, es decir, "la gente tiene preferencias diferentes aquí".

El uso de una sola conexión necesitaría una gran función mapStateToProps para transformar el estado hasta, por ejemplo, una jerarquía profunda de 10 componentes ... Estoy un poco confundido sobre cuál es el pensamiento detrás de eso o si estoy entendiendo mal algo ...

El uso de una sola conexión necesitaría una función mapStateToProps enorme

Nadie aboga por una sola conexión.

Luego, empaquetamos los componentes que queremos conectar a Redux con la función connect () de react-redux. Intente hacer esto solo para un componente de nivel superior o controladores de ruta . Si bien técnicamente puede conectar () cualquier componente de su aplicación a la tienda Redux, evite hacer esto con demasiada profundidad porque hará que el flujo de datos sea más difícil de rastrear.

El "single" solo se refiere a aplicaciones pequeñas como la que creamos en el ejemplo. No dude en modificar los documentos para aclararlo mejor. Ahora estoy ocupado con otros proyectos, así que no espere que este tema se mueva a menos que alguien haga un PR. Puedes hacerlo también.

Gracias por la aclaración.

Finalmente llegué a lanzar react-redux-provide . Compruébalo aquí . También lanzaré un puñado de otras cosas en los próximos días / semanas.

Eso se ve muy bien, gracias!

Acabo de comenzar un nuevo proyecto redux y estoy usando connect para componentes 'inteligentes'; esto tiene mucho más sentido para mí, y si hay un beneficio de rendimiento, hay una ganancia adicional. Por el contrario, si transfirió todo el control a la aplicación principal o los enrutadores, hay una pérdida completa de SRP: ¿qué tan grande debe ser su aplicación antes de comenzar a descomponer las cosas?

Incluso estoy pensando en organizar elementos relacionados en la carpeta de componentes, es decir. poner un reductor al lado de su componente principal, etc.

En última instancia, creo que redux / flux son un gran beneficio para el estado predecible, pero un cambio mental del mv estándar, lo que sea que haya hecho que el desarrollo de aplicaciones de interfaz de usuario sea simple y accesible para cualquiera, que eventualmente el flujo se abstraerá y nos mudaremos de nuevo a algo que se parece más a mv *.

Esto se está arreglando en el # 1285.

Ya no desaconsejamos la creación de componentes de contenedor en los documentos actualizados.
http://redux.js.org/docs/basics/UsageWithReact.html

Oye, escribí sobre algunas cosas que podrían ayudar aquí. :)

https://medium.com/@timbur/react -automatic-redux-proveedores-y-replicadores-c4e35a39f1

Creo que https://github.com/reactjs/redux/issues/419#issuecomment -183769392 también puede ayudar con el # 1353.

@timbur ¡ Impresionante artículo! ¿Podría compartir también sus pensamientos sobre esta pregunta: https://github.com/reactjs/react-redux/issues/278

No estoy seguro de si esto es deslumbrantemente obvio, pero creo que vale la pena agregarlo aquí para mayor claridad, ya que creo que mucho de lo que se ha dicho es quizás un poco abstracto para alguien nuevo en redux que viene a este hilo.

Cuando comencé a usar redux, coloqué erróneamente "contenedores" (componentes conectados) dentro (simplemente viejos y tontos) "componentes" porque pensé que mi aplicación no iba a requerir mucho desacoplamiento. Qué equivocado estaba. Cuando me di cuenta de que necesitaba reutilizar muchos de estos componentes, tuve que hacer un poco de refactorización y mover muchas cosas inteligentes directamente a la parte superior del árbol, pero esto pronto se volvió difícil de manejar; un "proveedor" en la parte superior que proporciona el contexto (como debería) pero también básicamente TODA la aplicación (que no debería).

La mejor forma que he encontrado para abordarlo es tener una jerarquía de contenedores que componen componentes tontos, con un proveedor en la parte superior. Los contenedores solo deben vivir dentro de otros contenedores . Su aplicación debe ser una jerarquía de contenedores que usen componentes para presentar sus datos.

A menudo, una buena forma de hacer esto con listas es pasar las ID a través de componentes inteligentes. La necesidad de una identificación es una señal de que algo pertenece al dominio de la aplicación. Entonces, cuando sea posible, tome listas de ID en un contenedor y páselos a otro contenedor que pueda usar los ID para obtener la información que desea. Dentro de cada contenedor, use un componente para representar esa información sin la necesidad de la identificación.

A continuación, me he burlado de un ejemplo (complicado) de cómo pasar las partes conectadas de la aplicación a través de una jerarquía de contenedores que usa componentes para mostrarlos.

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

Entonces, aquí la jerarquía de contenedores es TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Cada uno de ellos se representa entre sí, nunca se representa dentro de un componente y no contiene ningún código de vista sin formato (aparte del div ocasional por razones de ajuste de React).

En última instancia, la forma en que me gusta pensarlo es como si inicialmente crearas un árbol de componentes conectados que mapeen tus datos de una manera útil que a tu IU le interese. Sin embargo, no hay ninguna interfaz de usuario, solo un árbol de estado mapeado a través de una jerarquía de solo contenedores. Después de eso, revisa y esparce componentes de presentación dentro de esos contenedores para mostrar los datos de la forma que desee (es decir, en el DOM). Obviamente, no es útil escribir su aplicación de esta manera de dos pasos, pero me resultó útil conceptualizar el modelo de esta manera.

¿Existe una penalización de rendimiento (u otra razón arquitectónica) por usar connect() más de una vez? Estoy tratando de proporcionar accesorios de uso frecuente a los componentes abstrayendo su punto de entrada de conexión de la siguiente manera:

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

Estoy siendo vago aquí y no combiné las dos versiones de mapStateToProps y las dos versiones de mapDispatchToProps ya que mantiene la declaración simple. Pero me pregunto si es una mala idea dejar que connect haga ese trabajo por mí.

@timotgl : Dan ya no es un mantenedor activo; por favor, no le haga ping directamente.

Estaba a punto de decir que su pregunta está respondida en la entrada de Preguntas frecuentes de Redux sobre la conexión de múltiples componentes , pero parece que está preguntando sobre algo diferente: ¿envolver deliberadamente múltiples conexiones alrededor de un solo componente? No puedo decir que haya visto a alguien hacer eso antes, y he visto un _ lote_ de código Redux.

Personalmente, sugiero probar un enfoque diferente. O tenga un HOC de "accesorios comunes" o algo que en su mayoría simplemente los pase a sus hijos, o use selectores para recuperar los accesorios comunes en la función mapState del componente específico y combínelos con los accesorios específicos necesarios.

@markerikson Lo siento, no sabía eso, se eliminó la mención al

Entonces, en primer lugar, funciona y el componente aparece como cualquier otro componente conectado en las herramientas de desarrollo de react, no tiene un contenedor adicional ni nada de eso.

Me decidí en contra de un HOC porque no quería involucrar el paradigma OOP / herencia, ya que se trata de proporcionar más accesorios al componente, su comportamiento no se modifica.

Buen punto sobre hacer el cableado en mapStateToProps . Eso funcionaría, pero luego tengo al menos 2 puntos de entrada: llamar a una función auxiliar para conectarse parece más sencillo.

No estoy seguro de lo que quiere decir con "dos puntos de entrada".

Lo que estoy imaginando es algo como esto:

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 Por dos puntos de entrada quise decir que tendrías que hacer lo mismo por mapDispatchToProps , y potencialmente por mergeProps .

Casi nunca debería usar mergeProps ; está ahí como una escotilla de escape de último recurso, y desaconsejamos su uso. En general, también recomiendo que no escriba una función mapDispatch real y que utilice la "abreviatura del objeto" en su lugar:

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

class TodoList extends Component {}

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

Fácilmente podría tener algún archivo index.js que reexporta todos sus creadores de acciones "comunes" y hacer algo como:

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

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

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

Casi nunca debería usar mergeProps ; está ahí como una escotilla de escape de último recurso, y desaconsejamos su uso.

Hola @markerikson , solo tengo curiosidad por saber por qué alguien debería evitar usar mergeProps. Me resulta muy conveniente "ocultar" los accesorios de mapStateToProps que podría necesitar en mis acciones en mapDispatchToProps pero no en el componente. ¿Es algo malo?

¿Fue útil esta página
0 / 5 - 0 calificaciones