Redux: Uso recomendado de conectar ()

Criado em 7 ago. 2015  ·  34Comentários  ·  Fonte: reduxjs/redux

Olá @gaearon - a seguinte declaração me pegou desprevenido enquanto lia a documentação:

Em seguida, envolvemos os componentes que queremos conectar ao Redux com a função connect () do react-redux. Tente fazer isso apenas para um componente de nível superior ou manipuladores de rota. Embora tecnicamente você possa conectar () qualquer componente em seu aplicativo ao armazenamento Redux, evite fazer isso muito profundamente porque tornará o fluxo de dados mais difícil de rastrear.

Deep prop chains foi um dos problemas com React que me levou a usar o Flux; a desvantagem de tornar mais fácil rastrear o fluxo de dados é que você precisa configurar / manter / rastrear o fluxo de prop e, mais importante, os pais precisam saber sobre os requisitos de dados de seus filhos. Pela minha experiência, não estou convencido de que essa abordagem seja melhor, mas estou curioso para ouvir o que você pensa: sorria:

docs

Comentários muito úteis

Existem outros problemas além de rastrear o fluxo de dados ao conectar () --ing muitos componentes? Haveria uma penalidade de desempenho, por exemplo?

Não, é exatamente o oposto: você obtém melhor desempenho desistindo do fluxo de cima para baixo.

Todos 34 comentários

Eu concordo totalmente com Brent. Este foi muito do pensamento fundamental por trás de algo como Relay e leva a uma maior coesão.

Talvez devêssemos apenas dizer “as pessoas têm preferências diferentes aqui”.

@gaearon - talvez uma seção sobre as compensações que inclua uma demonstração de como isso complica o rastreamento do fluxo de dados seja útil. Eu poderia contribuir com o meu lado das preferências por isso.

Legal. Vamos mantê-lo aberto e revisitar depois que os documentos iniciais se estabelecerem.

Parece bom @gaearon! :) Envie-me um ping quando quiser revisitar

Eu sou muito novo no Redux, mas isso tem sido um grande problema para mim durante o ano passado, criando um aplicativo onde mudamos bastante a estrutura de dados. Então, devo dizer que realmente concordo com @brentvatne nesse ponto.

Existem outros problemas além de rastrear o fluxo de dados ao conectar () --ing muitos componentes? Haveria uma penalidade de desempenho, por exemplo?

Existem outros problemas além de rastrear o fluxo de dados ao conectar () --ing muitos componentes? Haveria uma penalidade de desempenho, por exemplo?

Não, é exatamente o oposto: você obtém melhor desempenho desistindo do fluxo de cima para baixo.

Porque você pode evitar re-renderizações desnecessárias de componentes de nível médio?

sim.

Para aumentar a discussão: estou limitando principalmente meu uso de conectar a componentes de rota. Quando eu tenho uma página que poderia usar um componente inteligente extra (por exemplo, um modal de formulário), minha solução alternativa tem sido passar o elemento ou nó e fazer com que o componente burro o renderize. Isso significa que você tem um pouco mais de clichê, mas testar o componente idiota ainda é fácil. Ainda estou fazendo experiências com isso, mas acho que essa pode ser a melhor maneira de compor componentes inteligentes sem abrir mão da testabilidade fácil.

Para dar um exemplo 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 Se você quiser escrever algo, descubra onde encaixá-lo na estrutura de documento atual e fique à vontade para ir em frente! :-)

Atualização de 19/10/2015: melhorei este comentário e o react-redux-provide . Consulte https://github.com/rackt/redux/issues/419#issuecomment -149325401 abaixo.

Continuando a partir de # 475, descreverei o que descobri, embora esta discussão provavelmente agora pertença a react-redux . ;)

Provedores modulares usando atribuição lateral

Resumindo, a abordagem que usei gira em torno de provedores modulares que você pode atribuir a qualquer componente. Isso permite componentes _verdadeiramente_ "burros", impõe uma separação máxima de interesses e torna possível usar e compartilhar de maneira muito fácil e rápida qualquer número de provedores intercambiáveis. Ele também impõe uma maneira mais eficiente de atualizar os componentes.

Então ... eu me livrei dos diretórios actions , constants , containers , reducers e stores (comuns nos exemplos ) e os substituiu por um único diretório providers . Como alternativa, um diretório providers pode nem mesmo ser necessário, pois essa abordagem permitiria que provedores independentes fossem empacotados e distribuídos. Acho que veremos coisas muito legais surgirem se essa abordagem em particular for adotada !! (Observação: é claro que não é necessário consolidar esses diretórios em um, mas acho que 1) torna as coisas muito mais fáceis de ler / entender e 2) reduz o clichê e 3) os provedores individuais são pequenos o suficiente para fazer sentido. )

Exemplo de componente "mudo"

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

Provedor de exemplo

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

Atribuição lateral

Como mencionado, a ideia é poder atribuir facilmente provedores arbitrários a componentes "burros". Portanto, ao montar seu aplicativo, você pode fazer isso desta forma:

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

Esperançosamente, tudo isso é bastante simples, mas ficaria feliz em entrar em mais detalhes e adicionar comentários para esclarecimento, se necessário.

Código Adicional

Você provavelmente notará um punhado de funções utilitárias sendo importadas nesses exemplos. Estes são módulos minúsculos (3 dos 4 têm apenas algumas linhas) que são basicamente apenas uma combinação dos métodos redux e react-redux existentes, projetados para os casos de uso mais comuns. É claro que você não está limitado a essas funções. Eles existem apenas para tornar as coisas ainda mais fáceis.

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

E por último, mas não menos importante, temos o decorador provide . É uma versão modificada de connect projetada para permitir a atribuição 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;
}

Uma coisa que vale a pena mencionar sobre o decorador provide é que se você olhar para tudo usando react-devtools , verá algo assim ( Branches fica embrulhado em ProvideBranches(theme,packageList,sources) ):
2015-08-15-010648_1366x768_scrot

Em vez disso (que você veria com o connect , onde Branches é embrulhado com Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Limitações

Eu comecei a aprender cerca de redux há apenas dois dias, então, honestamente, não tenho ideia das limitações (se houver) como resultado dessa abordagem. De repente, não consigo pensar em nenhum, e tudo parece funcionar perfeitamente para meus casos de uso, mas talvez alguém mais experiente possa intervir.

Eu vim com essa abordagem particular porque eu realmente gosto da ideia de ter componentes _verdadeiramente_ "burros" que podem ter qualquer provedor atribuído a eles. Ele impõe uma verdadeira separação de interesses e permite qualquer número de provedores facilmente intercambiáveis ​​como módulos.

Além disso, se @gaearon gostar dessa abordagem e achar que ela se enquadra no escopo de react-redux , ficaria feliz em enviar um PR com as adições. Do contrário, provavelmente criarei e publicarei react-redux-providers e, eventualmente, publicarei exemplos de provedores (por exemplo, coisas como react-redux-provide-toggle ) junto com alguns exemplos do mundo real.

Porque você pode evitar re-renderizações desnecessárias de componentes de nível médio?

sim

A direção que tomamos agora é que vamos usar Immutable.js para nosso estado (estado do aplicativo e estado da interface do usuário; mantemos algum estado da interface do usuário nos componentes) e, em seguida, usar o PureRenderMixin para todos os nossos componentes. Isso não eliminaria a penalidade de desempenho de usar o fluxo de cima para baixo?

Edit: acabei de perceber que isso não eliminará a penalidade de desempenho para componentes de nível médio se um de seus componentes de nível inferior ainda precisar renderizar novamente, mas ainda deve eliminar muito a sobrecarga. Alguma experiência com isso?

@ danmaz74 Temos encontrado problemas de desempenho em cenários como esse (não usando redux, mas uma biblioteca caseira muito semelhante). Porém, temos um aplicativo bastante complexo, com alguns componentes muito "caros". Além disso, conforme um aplicativo fica maior, injetar dados em mais lugares do que apenas no nível superior pode ajudar a evitar a criação de dependências implícitas entre os componentes e evitar que os pais saibam muito sobre os requisitos de dados de seus filhos.

@eldh obrigado pela atualização, isso faz sentido. Manteremos isso em mente ao prosseguir :)

Alguma atualização sobre isso? O exemplo de timbur faz todo o sentido para mim, enquanto a prática de conexão única eu não entendo muito bem. Eu estaria interessado no contra-argumento, ou seja, “as pessoas têm preferências diferentes aqui”.

Usando uma única conexão, seria necessária uma enorme função mapStateToProps para transformar o estado em, por exemplo, uma hierarquia de 10 componentes ... Estou um pouco confuso qual é o pensamento por trás disso ou se estou entendendo mal alguma coisa ...

Usando uma única conexão, seria necessária uma grande função mapStateToProps

Ninguém defende uma única conexão.

Em seguida, envolvemos os componentes que queremos conectar ao Redux com a função connect () do react-redux. Tente fazer isso apenas para um componente de nível superior ou manipuladores de rota . Embora tecnicamente você possa conectar () qualquer componente em seu aplicativo ao armazenamento Redux, evite fazer isso muito profundamente porque tornará o fluxo de dados mais difícil de rastrear.

O "único" se refere apenas a pequenos aplicativos como o que criamos no exemplo. Sinta-se à vontade para alterar os documentos para esclarecer melhor isso. Agora estou ocupado com outros projetos, então, por favor, não espere que esse problema tenha qualquer movimento, a menos que alguém faça um PR. Você também pode fazer isso.

Obrigado pelo esclarecimento.

Finalmente consegui liberar react-redux-provide . Confira aqui . Também estarei lançando um punhado de outras coisas nos próximos dias / semanas.

Isso parece muito bom, obrigado!

Acabei de iniciar um novo projeto de redux e estou usando o Connect para componentes 'inteligentes' - isso faz muito mais sentido para mim, e se houver um benefício de desempenho, há uma vitória extra. Por outro lado, se você distribuiu todo o controle para o aplicativo principal ou roteadores, ocorre uma perda completa do SRP - quão grande seu aplicativo precisa chegar antes de você começar a quebrar as coisas?

Estou até pensando em organizar os elementos relacionados em uma pasta de componentes - ou seja, coloque um redutor ao lado de seu componente principal, etc.

Em última análise, acredito que redux / fluxo são um grande benefício para o estado previsível, mas tal mudança mental do mv padrão, seja o que for que tornou o desenvolvimento de aplicativos de interface do usuário simples e acessível a qualquer pessoa, que eventualmente o fluxo será abstraído e nós mudaremos de volta para algo que se parece mais com mv *.

Isso está sendo corrigido em # 1285.

Não desencorajamos mais a criação de componentes de contêiner nos documentos atualizados.
http://redux.js.org/docs/basics/UsageWithReact.html

Ei, eu escrevi sobre algumas coisas que podem ajudar aqui. :)

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

Acho que https://github.com/reactjs/redux/issues/419#issuecomment -183769392 também pode ajudar com # 1353.

@timbur Artigo incrível! Você também poderia compartilhar sua opinião sobre esta questão: https://github.com/reactjs/react-redux/issues/278

Não tenho certeza se isso é extremamente óbvio, mas acho que vale a pena adicioná-lo aqui para maior clareza, pois acho que muito do que foi dito talvez seja um pouco abstrato para alguém novo em redux que vem para este tópico.

Quando comecei a usar o redux, coloquei erroneamente "contêineres" (componentes conectados) dentro (simplesmente e idiotas) "componentes" porque pensei que meu aplicativo não exigiria muita dissociação. Quão errado eu estava. Quando percebi que precisava reutilizar muitos desses componentes, tive que fazer uma boa refatoração e mover um monte de coisas inteligentes direto para o topo da árvore, mas isso logo se tornou difícil de manejar; um "provedor" no topo fornecendo o contexto (como deveria), mas também basicamente TODO o aplicativo (o que não deveria).

A maneira que achei melhor abordar isso é ter uma hierarquia de contêineres que compõem componentes burros, com um provedor no topo. Os contêineres devem viver apenas dentro de outros contêineres . Seu aplicativo deve ser uma hierarquia de contêineres que usam componentes para apresentar seus dados.

Freqüentemente, uma boa maneira de fazer isso com listas é passar IDs por meio de componentes inteligentes. A necessidade de um ID é um sinal de que algo pertence ao domínio do aplicativo. Então, sempre que possível, pegue listas de IDs em um contêiner e passe-as para outro contêiner que pode usar os IDs para obter as informações desejadas. Dentro de cada contêiner, use um componente para processar essas informações sem a necessidade do ID.

Abaixo, fiz uma simulação de um exemplo (complicado) de como transmitir as partes conectadas do aplicativo por meio de uma hierarquia de contêineres que usa componentes para exibi-los.

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

Portanto, aqui a hierarquia do contêiner é TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Eles são renderizados um pelo outro, nunca renderizados dentro de um componente e não contêm código de visualização bruto (exceto div ocasionais por motivos de agrupamento do React).

No final das contas, gosto de pensar nisso como se você criasse inicialmente uma árvore de componentes conectados que mapeiam seus dados de uma forma útil com a qual sua IU _will_ se preocupa. No entanto, não há nenhuma IU, apenas uma árvore de estado mapeada por meio de uma hierarquia de apenas contêineres. Depois disso, você percorre e espalha componentes de apresentação dentro desses contêineres para realmente exibir os dados da maneira que quiser (ou seja, no DOM). Obviamente, não é útil escrever seu aplicativo dessa forma de duas passagens, mas achei útil conceituar o modelo dessa forma.

Há uma penalidade de desempenho (ou outro motivo arquitetônico) por usar connect() mais de uma vez? Estou tentando fornecer adereços usados ​​com frequência para componentes abstraindo seu ponto de entrada de conexão da seguinte maneira:

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

Estou sendo preguiçoso aqui e não combinei as duas versões de mapStateToProps e as duas versões de mapDispatchToProps pois mantém a declaração simples. Mas estou me perguntando se é uma má ideia deixar connect fazer esse trabalho para mim.

@timotgl : Dan não é mais um mantenedor ativo - por favor, não execute ping diretamente nele.

Eu estava prestes a dizer que sua pergunta foi respondida na entrada Redux FAQ sobre como conectar vários componentes , mas parece que você está perguntando sobre algo diferente - deliberadamente envolver várias conexões em um único componente? Não posso dizer que já vi alguém fazer isso antes, e já vi _muito_ do código Redux.

Pessoalmente, sugiro tentar uma abordagem diferente. Tenha um HOC de "adereços comuns" ou algo que apenas os repasse para seus filhos, ou use seletores para recuperar os adereços comuns na função mapState do componente específico e combine-os com os adereços específicos necessários.

@markerikson Desculpe, não sabia disso, uma menção removida.

Então, em primeiro lugar, ele funciona, e o componente aparece como qualquer outro componente conectado nas ferramentas react dev, não tem um invólucro adicional ou algo parecido.

Decidi não fazer um HOC porque não queria envolver o paradigma OOP / herança, já que se trata apenas de fornecer mais alguns adereços ao componente, seu comportamento permanece intocado.

Bom ponto sobre como fazer a fiação em mapStateToProps . Isso funcionaria, mas eu tenho pelo menos 2 pontos de entrada - chamar uma função auxiliar para conectar parece mais simples.

Não tenho certeza do que você quer dizer com "dois pontos de entrada".

O que estou imaginando é algo assim:

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 dois pontos de entrada, quis dizer que você teria que fazer o mesmo para mapDispatchToProps e, potencialmente, para mergeProps .

Você quase nunca deveria usar mergeProps - ele está lá como uma saída de emergência de último recurso e nós desencorajamos seu uso. Eu também geralmente recomendo que você não escreva uma função mapDispatch real e use a "abreviação do objeto" em seu lugar:

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

class TodoList extends Component {}

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

Você poderia facilmente ter algum arquivo index.js que reexporta todos os seus criadores de ação "comuns" e fazer 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);

Você quase nunca deveria usar mergeProps - ele está lá como uma saída de emergência de último recurso e nós desencorajamos seu uso.

Olá @markerikson , estou apenas curioso para saber por que alguém deve evitar o uso de mergeProps? Acho muito conveniente "ocultar" adereços de mapStateToProps que eu poderia precisar em minhas ações em mapDispatchToProps, mas não no componente. Isso é uma coisa ruim?

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

caojinli picture caojinli  ·  3Comentários

captbaritone picture captbaritone  ·  3Comentários

vraa picture vraa  ·  3Comentários

vslinko picture vslinko  ·  3Comentários

jimbolla picture jimbolla  ·  3Comentários