Redux: connect() 的推荐用法

创建于 2015-08-07  ·  34评论  ·  资料来源: reduxjs/redux

@gaearon - 在阅读文档时,以下声明让我措手不及:

然后,我们使用 react-redux 中的 connect() 函数包装要连接到 Redux 的组件。 尝试仅对顶级组件或路由处理程序执行此操作。 虽然从技术上讲,您可以将应用程序中的任何组件 connect() 到 Redux 存储,但避免这样做太深,因为它会使数据流更难跟踪。

深度 prop 链是 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如果您想写一些东西,请找出将其放入当前文档结构的位置,然后继续! :-)

2015/10/19 更新:我改进了此评论并将其发布为react-redux-provide 。 请参阅下面的https://github.com/rackt/redux/issues/419#issuecomment -149325401。

从#475 继续,我将描述我的想法,尽管这个讨论现在可能属于react-redux 。 ;)

使用横向分配的模块化提供程序

长话短说,我采用的方法围绕着可以分配给任何组件的模块化提供程序。 这允许_真正的_“哑”组件,强制最大程度地分离关注点,并使非常容易和快速地使用和共享任意数量的可互换提供程序成为可能。 它还强制执行一种更有效的方法来更新组件。

所以......我摆脱了actionsconstantscontainersreducersstores目录(在示例中很常见) ) 并将它们替换为单个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')
);

希望这一切都非常简单,但如果有必要,我很乐意提供更多细节并添加注释以进行澄清。

附加代码

您可能会注意到在这些示例中导入了一些实用函数。 这些是小模块(4 个中的 3 个只有几行长),它们基本上只是现有reduxreact-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 ,其中BranchesConnect(Branches)包裹):
2015-08-14-220140_1366x768_scrot

限制

我两天前才开始了解redux ,所以老实说,我不知道这种方法存在什么(如果有)限制。 在我的头顶上,我真的想不出任何东西,而且一切似乎都适合我的用例,但也许更有经验的人可以插话。

我想出了这个特殊的方法,因为我真的很喜欢拥有 _truly_ “哑”组件的想法,这些组件可以分配给它们的任何提供程序。 它强制实现真正的关注点分离,并允许将任意数量的可轻松互换的提供者作为模块。

此外,如果@gaearon喜欢这种方法并认为它属于react-redux ,我很乐意提交带有添加内容的 PR。 如果没有,我可能会创建并发布react-redux-providers ,然后最终发布示例提供程序(例如,诸如react-redux-provide-toggle类的东西)以及一些真实世界的示例。

因为您可以避免不必要的中级组件重新渲染?

是的

我们现在采取的方向是我们将使用 Immutable.js 作为我们的状态(应用程序状态和 UI 状态;我们在组件中保留一些 UI 状态),然后对我们所有的组件使用 PureRenderMixin。 这不会消除使用自上而下流程的性能损失吗?

编辑:我刚刚意识到,如果其中一个较低级别的组件仍然需要重新渲染,它不会消除对中级组件的性能损失,但它仍然应该消除很多开销。 有这方面的经验吗?

@ danmaz74我们在这样的场景中遇到了性能问题(不使用 redux,而是使用非常相似的自制库)。 不过,我们有一个非常复杂的应用程序,其中包含一些非常“昂贵”的组件。 此外,随着应用程序变得越来越大,在更多的地方而不是仅仅在顶层注入数据可以帮助您避免在组件之间创建隐式依赖关系,并避免让父母对孩子的数据需求了解太多。

@eldh感谢您的更新,这是有道理的。 我们会在继续的过程中牢记这一点:)

有任何更新吗? timbur 的例子对我来说非常有意义,而我不太明白单连接练习。 我会对相反的论点感兴趣,即“人们在这里有不同的偏好”。

使用单个连接需要一个巨大的 mapStateToProps 函数来将状态一直转换为例如 10 个组件的深层层次结构......我有点困惑这背后的想法是什么,或者我是否误解了什么......

使用单个连接需要一个巨大的 mapStateToProps 函数

没有人提倡单一连接。

然后,我们使用 react-redux 中的 connect() 函数包装要连接到 Redux 的组件。 尝试仅对顶级组件或路由处理程序执行此操作。 虽然从技术上讲,您可以将应用程序中的任何组件 connect() 到 Redux 存储,但避免这样做太深,因为它会使数据流更难跟踪。

“单个”仅指我们在示例中创建的小型应用程序。 请随时修改文档以更好地阐明这一点。 我现在正忙于其他项目,所以除非有人提出 PR,否则请不要指望这个问题有任何进展。 你也可以做到。

谢谢你的澄清。

终于开始发布react-redux-provide在这里查看。 在接下来的几天/几周内,我还将发布一些其他内容。

看起来非常好,谢谢!

我刚刚开始了一个新的 redux 项目,并且正在将 connect 用于“智能”组件——这对我来说更有意义,如果有性能优势,那就是额外的胜利。 相反,如果您将所有控制权交给主应用程序或路由器,则 SRP 将完全丢失 - 在您开始分解事物之前,您的应用程序必须有多大?

我什至在考虑将相关元素组织到组件文件夹中 - 即。 在它的主要部件旁边放一个减速器等。

最终,我相信 redux/flux 对可预测的状态是一个巨大的好处,但是这种从标准 mv 的心理转变——无论使 UI 应用程序开发变得简单且对任何人都可以访问,最终 flux 将被抽象掉,我们将移动回到看起来更像 mv* 的东西。

这已在 #1285 中修复。

我们不再鼓励在更新的文档中创建容器组件。
http://redux.js.org/docs/basics/UsageWithReact.html

嘿,我写了一些可能对这里有帮助的东西。 :)

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

我认为https://github.com/reactjs/redux/issues/419#issuecomment -183769392 也可以帮助解决 #1353。

@timbur很棒的文章! 您能否也分享您对这个问题的看法: https :

我不确定这是否非常明显,但我觉得为了清楚起见,值得在此处添加它,因为我认为很多人所说的对于来到这个线程的 redux 新手来说可能有点抽象。

当我第一次开始使用 redux 时,我错误地将“容器”(连接的组件)加在了“组件”内部(只是普通的愚蠢的)“组件”,因为我认为我的应用程序不需要太多的解耦。 我错了。 当我意识到我需要重用其中的许多组件时,我不得不进行大量重构并将许多智能内容移到树的顶部,但这很快变得笨拙; 顶部的“提供者”提供上下文(它应该提供),但基本上也提供所有应用程序(它不应该提供)。

我发现最好的方法是拥有一个由容器组成的层次结构,这些容器组成哑组件,顶部有一个提供者。 容器应该只存在于其他容器内。 您的应用程序应该是使用组件来呈现其数据的容器层次结构。

通常对列表执行此操作的一种好方法是通过智能组件向下传递 ID。 需要一个 ID 表明某些东西属于应用程序的域。 因此,在可能的情况下,获取一个容器中的 ID 列表,并将它们传递给另一个容器,该容器可以使用这些 ID 来获取所需的信息。 在每个容器内使用一个组件来呈现该信息,而无需 ID。

下面我模拟了一个(复杂的)示例,说明如何通过使用组件来显示它们的容器层次结构传递应用程序的连接部分。

// 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 包装的原因)。

最终,我喜欢的想法是,好像您最初要创建一个连接组件树,将您的数据映射到您的 UI _will_ 关心的有用方式。 然而,没有任何用户界面,只是状态的树通过容器的层次结构映射。 之后,您将通过并在这些容器中添加展示组件,以您想要的任何方式(即在 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 :Dan 不再是活跃的维护者 - 请不要直接 ping 他。

我正要说你的问题在Redux FAQ entry on connected multiple components 中得到了回答,但看起来你在问一些不同的问题 - 故意将多个连接包裹在单个组件周围? 我不能说我以前见过任何人这样做过,而且我见过很多 Redux 代码。

就个人而言,我建议尝试不同的方法。 要么有一个“通用道具” HOC 或一些主要只是将它们传递给它的孩子的东西,或者使用选择器来检索特定组件的mapState函数中的通用道具,并将它们与所需的特定道具结合起来。

@markerikson抱歉不知道,提及已删除。

所以首先,它可以工作,并且该组件看起来像 react 开发工具中的任何其他连接组件,它没有额外的包装器或类似的东西。

我决定反对 HOC,因为我不想涉及 OOP/继承范式,因为它只是为组件提供更多道具,否则它的行为不会受到影响。

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 等级

相关问题

rui-ktei picture rui-ktei  ·  3评论

CellOcean picture CellOcean  ·  3评论

olalonde picture olalonde  ·  3评论

vraa picture vraa  ·  3评论

elado picture elado  ·  3评论