Redux: Рекомендуемое использование connect ()

Созданный на 7 авг. 2015  ·  34Комментарии  ·  Источник: reduxjs/redux

Привет, @gaearon - следующее заявление застало меня врасплох при чтении документации:

Затем мы обертываем компоненты, которые хотим подключиться к Redux, с помощью функции connect () из react-redux. Попробуйте сделать это только для компонента верхнего уровня или обработчиков маршрутов. Хотя технически вы можете подключить () любой компонент в своем приложении к хранилищу Redux, не делайте этого слишком глубоко, потому что это затруднит отслеживание потока данных.

Глубокие цепочки пропеллеров были одной из проблем с React, которые побудили меня использовать Flux; компромисс для упрощения отслеживания потока данных заключается в том, что вам необходимо настроить / поддерживать / отслеживать поток данных, и, что более важно, родители должны знать о требованиях к данным своих детей. По своему опыту я не уверен, что этот подход лучше, но мне любопытно услышать, что вы думаете: smile:

Самый полезный комментарий

Есть ли другие проблемы, кроме отслеживания потока данных при connect () большого количества компонентов? Например, будет ли потеря производительности?

Нет, это как раз наоборот: вы получите лучшую производительность, отказавшись от нисходящего потока.

Все 34 Комментарий

Я полностью согласен с Брентом. Это было основной идеей, лежащей в основе чего-то вроде Relay, и привело к большей сплоченности.

Может быть, нам стоит просто сказать «у людей здесь разные предпочтения».

@gaearon - возможно, будет полезен раздел о компромиссах, который включает демонстрацию того, как это усложняет отслеживание потока данных. Я мог бы поддержать свои предпочтения для этого.

Прохладный. Давайте оставим его открытым и вернемся к нему после того, как начальные документы успокоятся.

Звучит хорошо @gaearon! :) Напишите мне, когда захотите еще раз

Я новичок в Redux, но в последний год для меня это было довольно большой проблемой - создание приложения, в котором мы довольно сильно изменили структуру данных. Так что должен сказать, что я действительно согласен с @brentvatne по этому

Есть ли другие проблемы, кроме отслеживания потока данных при connect () большого количества компонентов? Например, будет ли потеря производительности?

Есть ли другие проблемы, кроме отслеживания потока данных при connect () большого количества компонентов? Например, будет ли потеря производительности?

Нет, это как раз наоборот: вы получите лучшую производительность, отказавшись от нисходящего потока.

Потому что вы можете избежать ненужного повторного рендеринга компонентов среднего уровня?

да.

Чтобы добавить к обсуждению: я в основном ограничиваю использование подключения к компонентам маршрута. Когда у меня есть страница, которая может использовать дополнительный интеллектуальный компонент (например, модальную форму), я решил передать элемент или узел и заставить его визуализировать немой компонент. Это означает, что у вас немного больше шаблонов, но протестировать тупой компонент по-прежнему легко. Я все еще экспериментирую с этим, но я думаю, что это может быть лучший способ составить интеллектуальные компоненты, не отказываясь от легкости тестирования.

Приведу грубый пример:

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 Если вы хотите что-то написать,

Обновление от 19.10.2015: я улучшил этот комментарий и опубликовал его как react-redux-provide . См. Https://github.com/rackt/redux/issues/419#issuecomment -149325401 ниже.

Продолжая с # 475, я опишу то, что придумал, хотя это обсуждение, вероятно, теперь относится к react-redux . ;)

Модульные поставщики, использующие боковое назначение

Короче говоря, мой подход основан на модульных поставщиках, которые можно назначить любому компоненту. Это позволяет использовать _truly_ "тупые" компоненты, обеспечивает максимальное разделение задач и позволяет очень легко и быстро использовать и совместно использовать любое количество взаимозаменяемых поставщиков. Это также обеспечивает более эффективный способ обновления компонентов.

Итак ... Я избавился от каталогов actions , constants , containers , reducers и stores (часто встречается в примерах ) и заменил их одним каталогом providers . В качестве альтернативы каталог providers может даже не потребоваться, поскольку этот подход позволит упаковывать и распространять автономных поставщиков. Я думаю, мы увидим, что появятся действительно интересные вещи, если будет принят именно этот подход !! (Примечание: конечно, нет необходимости объединять эти каталоги в один, но я чувствую, что это 1) упрощает чтение / понимание, 2) сокращает шаблон, и 3) отдельные поставщики достаточно малы, чтобы иметь смысл. )

Пример «тупого» компонента

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

Пример поставщика

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

Боковое назначение

Как уже упоминалось, идея состоит в том, чтобы иметь возможность легко назначать произвольных поставщиков «тупым» компонентам. Итак, монтируя свое приложение, вы можете сделать это следующим образом:

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

Надеюсь, это все довольно просто, но я был бы рад более подробно остановиться и добавить комментарии для пояснений, если это необходимо.

Дополнительный код

Вы, вероятно, заметите, что в этих примерах импортируется несколько служебных функций. Это крошечные модули (3 из 4 имеют длину всего несколько строк), которые в основном представляют собой просто комбинацию существующих методов redux и react-redux , разработанных для наиболее распространенных случаев использования. Вы, конечно, не ограничены этими функциями. Они существуют только для того, чтобы все было еще проще.

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

И наконец, что не менее важно, у нас есть декоратор provide . Это модифицированная версия connect предназначенная для бокового назначения.

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

О декораторе provide стоит упомянуть то, что если вы посмотрите на все, используя react-devtools , вы увидите что-то вроде этого ( Branches обернут в ProvideBranches(theme,packageList,sources) ):
2015-08-15-010648_1366x768_scrot

Вместо этого (что вы увидите с исходным connect , где Branches обернут в Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Ограничения

Я только начал узнавать о redux два дня назад, поэтому я, честно говоря, понятия не имею, какие (если есть) ограничения существуют в результате такого подхода. Вне всяких сомнений, я не могу ни о чем думать, и, похоже, все работает идеально для моих сценариев использования, но, возможно, кто-то более опытный сможет вмешаться.

Я пришел к этому конкретному подходу, потому что мне очень нравится идея иметь _truly_ "тупые" компоненты, которым может быть назначен любой провайдер. Он обеспечивает истинное разделение задач и позволяет использовать любое количество легко заменяемых поставщиков в виде модулей.

Кроме того, если @gaearon нравится такой подход и он считает, что он подпадает под действие react-redux , я буду рад отправить PR с дополнениями. Если нет, я, вероятно, создам и опубликую react-redux-providers , а затем, в конечном итоге, опубликую примеры поставщиков (например, такие вещи, как react-redux-provide-toggle ) вместе с некоторыми примерами из реальной жизни.

Потому что вы можете избежать ненужного повторного рендеринга компонентов среднего уровня?

да

Направление, которое мы выбрали сейчас, заключается в том, что мы собираемся использовать Immutable.js для нашего состояния (как состояния приложения, так и состояния пользовательского интерфейса; мы сохраняем некоторое состояние пользовательского интерфейса в компонентах), а затем будем использовать PureRenderMixin для всех наших компонентов. Разве это не устранило бы снижение производительности при использовании нисходящего потока?

Изменить: я только что понял, что это не устранит этого снижения производительности для компонентов среднего уровня, если один из их компонентов более низкого уровня все еще нуждается в повторном рендеринге, но он все равно должен устранить много накладных расходов. Есть опыт работы с этим?

@ danmaz74 Мы

@eldh спасибо за обновление, это имеет смысл. Будем иметь это в виду в дальнейшем :)

Есть обновления по этому поводу? Пример Тимбура имеет для меня смысл, в то время как практика одиночного соединения мне не совсем понятна. Меня бы заинтересовал контраргумент, то есть «здесь у людей разные предпочтения».

При использовании одного соединения потребуется огромная функция mapStateToProps для полного преобразования состояния, например, до 10-компонентной глубокой иерархии ... Я немного сбит с толку, что за этим стоит, или я что-то неправильно понимаю ...

При использовании одного подключения потребуется огромная функция mapStateToProps.

Никто не защищает единую связь.

Затем мы обертываем компоненты, которые хотим подключиться к Redux, с помощью функции connect () из react-redux. Попробуйте сделать это только для компонента верхнего уровня или обработчиков маршрутов . Хотя технически вы можете подключить () любой компонент в своем приложении к хранилищу Redux, избегайте этого слишком глубоко, потому что это затруднит отслеживание потока данных.

«Одиночный» относится только к небольшим приложениям, подобным тому, которое мы создаем в примере. Пожалуйста, не стесняйтесь вносить поправки в документы, чтобы лучше прояснить это. Сейчас я занят другими проектами, поэтому, пожалуйста, не ожидайте, что этот выпуск получит какое-либо движение, если только кто-то не сделает пиар. Вы тоже можете это сделать.

Спасибо тебе за пояснение.

Наконец-то дошли до выпуска react-redux-provide . Посмотрите здесь . Я также выпущу несколько других вещей в течение следующих нескольких дней / недель.

Выглядит очень хорошо, спасибо!

Я только что начал новый проект redux и использую connect для «умных» компонентов - это имеет для меня гораздо больше смысла, и если есть преимущество производительности, есть дополнительная победа. И наоборот, если вы передадите все управление основному приложению или маршрутизаторам, SRP будет полностью утрачен - насколько большим должно стать ваше приложение, прежде чем вы начнете разбирать вещи?

Я даже думаю об организации связанных элементов в папке компонентов - т.е. поместите редуктор рядом с его основным компонентом и т. д.

В конечном счете, я считаю, что redux / flux - огромное преимущество для предсказуемого состояния, но такой мысленный сдвиг от стандартного mv-независимо от того, что сделало разработку UI-приложений простой и доступной для всех, что в конечном итоге поток будет абстрагирован, и мы перейдем вернемся к чему-то, что больше похоже на mv *.

Это исправлено в # 1285.

Мы больше не препятствуем созданию компонентов контейнера в обновленной документации.
http://redux.js.org/docs/basics/UsageWithReact.html

Привет, я написал кое-что, что могло бы здесь помочь. :)

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

Я думаю, что https://github.com/reactjs/redux/issues/419#issuecomment -183769392 также может помочь с # 1353.

@timbur Замечательная статья! Не могли бы вы также поделиться своими мыслями по этому вопросу: https://github.com/reactjs/react-redux/issues/278

Я не уверен, является ли это ослепительно очевидным, но я считаю, что стоит добавить это здесь для ясности, поскольку я думаю, что многое из того, что было сказано, возможно, немного абстрактно для кого-то, кто новичок в redux, пришедшего в эту ветку.

Когда я впервые начал использовать redux, я ошибочно расставил точки «контейнеры» (связанные компоненты) внутри (просто старых тупых) «компонентов», потому что думал, что мое приложение не потребует особой развязки. Как я ошибался. Когда я понял, что мне нужно повторно использовать многие из этих компонентов, мне пришлось провести изрядный рефакторинг и переместить много умных вещей прямо на вершину дерева, но это вскоре стало громоздким; «провайдер» вверху, обеспечивающий контекст (как должен), но также в основном ВСЕ приложение (чего не должно).

Я нашел лучший подход к этому - иметь иерархию контейнеров, составляющих глупые компоненты, с поставщиком наверху. Контейнеры должны жить только внутри других контейнеров . Ваше приложение должно представлять собой иерархию контейнеров, которые используют компоненты для представления своих данных.

Часто хороший способ сделать это со списками - передать идентификаторы через интеллектуальные компоненты. Необходимость в идентификаторе - это признак того, что что-то принадлежит домену приложения. Поэтому, где это возможно, возьмите списки идентификаторов в одном контейнере и передайте их другому контейнеру, который может использовать идентификаторы для получения нужной информации. В каждом контейнере используйте компонент для отображения этой информации без идентификатора.

Ниже я высмеял (запутанный) пример того, как передавать связанные части приложения через иерархию контейнеров, которая использует компоненты для их отображения.

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

Итак, здесь иерархия контейнеров: TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Каждый из них визуализируется друг другом, никогда не отображается внутри компонента и не содержит необработанного кода представления (за исключением случайного div по причинам упаковки React).

В конечном счете, мне нравится думать об этом так, как если бы вы изначально создавали дерево связанных компонентов, которые отображают ваши данные таким образом, чтобы ваш пользовательский интерфейс был заинтересован в этом. Однако нет никакого пользовательского интерфейса, просто дерево состояний, отображаемое через иерархию простых контейнеров. После этого вы просматриваете и разбрасываете презентационные компоненты внутри этих контейнеров, чтобы фактически отображать данные так, как вы хотите (то есть в DOM). Очевидно, что писать приложение таким двухпроходным способом бесполезно, но я счел полезным концептуализировать модель таким образом.

Есть ли снижение производительности (или другая архитектурная причина) для использования connect() более одного раза? Я пытаюсь предоставить часто используемые реквизиты для компонентов, абстрагируя их точку входа подключения следующим образом:

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

Я поленился и не объединил две версии mapStateToProps и две версии mapDispatchToProps поскольку это упрощает декларацию. Но мне интересно, не стоит ли позволять connect делать эту работу за меня.

@timotgl : Дэн больше не является активным сопровождающим - пожалуйста, не пингуйте его напрямую.

Я собирался сказать, что ответ на ваш вопрос содержится в разделе часто задаваемых вопросов Redux о подключении нескольких компонентов , но похоже, что вы спрашиваете о другом - намеренном обертывании нескольких соединений вокруг одного компонента? Я не могу сказать, что когда-либо видел, чтобы кто-то делал это раньше, и я видел много кода Redux.

Лично я предлагаю попробовать другой подход. Либо иметь HOC «общие свойства», либо что-то, что в основном просто передает их своим дочерним элементам, либо используйте селекторы для получения общих свойств в функции mapState конкретного компонента и комбинируйте их с конкретными необходимыми свойствами.

@markerikson Извините, не знал, упоминание удалено.

Итак, во-первых, он работает, и компонент выглядит как любой другой подключенный компонент в инструментах разработки реакции, у него нет дополнительной оболочки или чего-то подобного.

Я отказался от HOC, потому что я не хотел задействовать парадигму ООП / наследования, поскольку речь идет только о предоставлении дополнительных свойств компоненту, в остальном его поведение остается неизменным.

Хороший момент по поводу проводки в mapStateToProps . Это сработает, но тогда у меня есть как минимум 2 точки входа - вызов одной вспомогательной функции для подключения кажется более простым.

Не уверен, что вы имеете в виду под «двумя точками входа».

Я представляю что-то вроде этого:

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 Под двумя точками входа я имел в виду, что вам придется сделать то же самое для mapDispatchToProps и, возможно, для mergeProps .

Вы почти никогда не должны использовать mergeProps - это крайний выход, и мы не рекомендуем его использовать. Я также обычно рекомендую вам не писать настоящую функцию mapDispatch , а вместо этого использовать "сокращенную запись объекта":

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

class TodoList extends Component {}

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

Вы могли бы легко получить какой-нибудь файл index.js, который повторно экспортирует всех ваших «общих» создателей действий и сделает что-то вроде:

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

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

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

Вы почти никогда не должны использовать mergeProps - это крайний выход, и мы не рекомендуем его использовать.

Привет @markerikson , мне просто интересно, почему кому-то следует избегать использования mergeProps? Мне очень удобно «скрывать» реквизиты из mapStateToProps, которые мне могут понадобиться в моих действиях в mapDispatchToProps, но не в компоненте. Это плохо?

Была ли эта страница полезной?
0 / 5 - 0 рейтинги