Redux: Direkomendasikan penggunaan connect()

Dibuat pada 7 Agu 2015  ·  34Komentar  ·  Sumber: reduxjs/redux

Hai @gaearon - pernyataan berikut membuat saya lengah saat membaca dokumen:

Kemudian, kita membungkus komponen yang ingin kita hubungkan ke Redux dengan fungsi connect() dari react-redux. Coba lakukan ini hanya untuk komponen tingkat atas, atau penangan rute. Meskipun secara teknis Anda dapat menghubungkan() komponen apa pun di aplikasi Anda ke Redux store, hindari melakukan ini terlalu dalam karena akan membuat aliran data lebih sulit untuk dilacak.

Rantai penyangga yang dalam adalah salah satu masalah dengan React yang membuat saya menggunakan Flux; tradeoff untuk membuatnya lebih mudah untuk melacak aliran data adalah Anda perlu mengatur/memelihara/melacak aliran prop, dan yang lebih penting, orang tua perlu mengetahui tentang persyaratan data anak-anak mereka. Dari pengalaman saya, saya tidak yakin bahwa pendekatan ini lebih baik, tetapi saya ingin tahu apa yang Anda pikirkan :senyum:

docs

Komentar yang paling membantu

Apakah ada masalah lain selain melacak aliran data saat menghubungkan()-ing banyak komponen? Apakah akan ada penalti kinerja misalnya?

Tidak, justru sebaliknya: Anda mendapatkan kinerja yang lebih baik dengan menghentikan aliran top-down.

Semua 34 komentar

Saya sangat setuju dengan Brent. Ini adalah pemikiran mendasar di balik sesuatu seperti Relay dan mengarah ke kohesi yang lebih tinggi.

Mungkin kita harus mengatakan "orang memiliki preferensi yang berbeda di sini".

@gaearon - mungkin bagian tentang pengorbanan yang mencakup demonstrasi bagaimana hal itu memperumit penelusuran aliran data akan berguna. Saya bisa mengajukan preferensi untuk itu di sisi saya.

Dingin. Mari tetap buka dan kunjungi kembali setelah dokumen awal diselesaikan.

Kedengarannya bagus @gaearon! :) Ping saya ketika Anda ingin mengunjungi kembali

Saya cukup baru di Redux, tetapi ini telah menjadi masalah besar bagi saya selama setahun terakhir, membangun aplikasi di mana kami telah banyak mengubah struktur data. Jadi harus dikatakan saya sangat setuju dengan @brentvatne yang satu ini.

Apakah ada masalah lain selain melacak aliran data saat menghubungkan()-ing banyak komponen? Apakah akan ada penalti kinerja misalnya?

Apakah ada masalah lain selain melacak aliran data saat menghubungkan()-ing banyak komponen? Apakah akan ada penalti kinerja misalnya?

Tidak, justru sebaliknya: Anda mendapatkan kinerja yang lebih baik dengan menghentikan aliran top-down.

Karena Anda dapat menghindari rendering ulang komponen tingkat menengah yang tidak perlu?

Ya.

Untuk menambah diskusi: Saya kebanyakan membatasi penggunaan komponen koneksi ke rute. Ketika saya memiliki halaman yang dapat menggunakan komponen ekstra cerdas (misalnya modal formulir), solusi saya adalah dengan melewatkan elemen atau simpul, dan membuat komponen bodoh itu merendernya. Ini berarti Anda memiliki lebih banyak boilerplate, tetapi menguji komponen bodoh masih mudah. Saya masih bereksperimen dengan ini, tapi saya pikir ini mungkin cara terbaik untuk membuat komponen cerdas tanpa menyerah testability mudah.

Untuk memberikan contoh kasar:

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 Jika Anda ingin menulis sesuatu,

Pembaruan 2015/10/19: Saya telah memperbaiki komentar ini dan merilisnya sebagai react-redux-provide . Lihat https://github.com/rackt/redux/issues/419#issuecomment -149325401 di bawah.

Melanjutkan dari #475, saya akan menjelaskan apa yang telah saya hasilkan, meskipun diskusi ini mungkin sekarang termasuk dalam react-redux . ;)

Penyedia Modular Menggunakan Penugasan Sideways

Singkat cerita, pendekatan yang saya ambil berkisar pada penyedia modular yang dapat Anda tetapkan ke komponen apa pun. Hal ini memungkinkan untuk _benar-benar_ komponen "bodoh", memaksakan pemisahan maksimum dari masalah, dan memungkinkan untuk dengan sangat mudah dan cepat menggunakan dan berbagi sejumlah penyedia yang dapat dipertukarkan. Ini juga memberlakukan cara yang lebih efisien untuk memperbarui komponen.

Jadi... saya menyingkirkan direktori actions , constants , containers , reducers , dan stores (umum dalam contoh ) dan menggantinya dengan satu direktori providers . Atau, direktori providers bahkan mungkin tidak diperlukan, karena pendekatan ini memungkinkan penyedia mandiri untuk dikemas dan didistribusikan. Saya pikir kita akan melihat beberapa hal yang sangat keren muncul jika pendekatan khusus ini diadopsi!! (Catatan: Tentu saja tidak perlu mengkonsolidasikan direktori-direktori itu menjadi satu, tetapi saya merasa itu 1) membuat segalanya lebih mudah dibaca/dipahami dan 2) mengurangi boilerplate dan 3) penyedia individu cukup kecil sehingga masuk akal. )

Contoh Komponen "Bodoh"

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

Penyedia Contoh

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

Tugas sampingan

Seperti disebutkan, idenya adalah untuk dapat dengan mudah menetapkan penyedia sewenang-wenang ke komponen "bodoh". Jadi saat memasang aplikasi Anda, Anda dapat melakukannya seperti ini:

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

Mudah-mudahan ini semua cukup mudah, tetapi saya akan senang untuk membahas lebih detail dan menambahkan komentar untuk klarifikasi jika perlu.

Kode Tambahan

Anda mungkin akan melihat beberapa fungsi utilitas yang diimpor dalam contoh ini. Ini adalah modul kecil (3 dari 4 hanya beberapa baris panjangnya) yang pada dasarnya hanya kombinasi dari metode redux dan react-redux yang ada, yang dirancang untuk kasus penggunaan yang paling umum. Anda tentu saja tidak terbatas pada fungsi-fungsi ini. Mereka hanya ada untuk membuat segalanya lebih mudah.

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

Dan yang tak kalah pentingnya, kami memiliki dekorator provide . Ini adalah versi modifikasi dari connect dirancang untuk mengaktifkan penugasan menyamping.

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

Satu hal yang perlu disebutkan tentang provide dekorator adalah jika Anda melihat semuanya menggunakan react-devtools , Anda akan melihat sesuatu seperti ini ( Branches dibungkus ProvideBranches(theme,packageList,sources) ):
2015-08-15-010648_1366x768_scrot

Alih-alih ini (yang akan Anda lihat dengan connect , di mana Branches dibungkus dengan Connect(Branches) ):
2015-08-14-220140_1366x768_scrot

Keterbatasan

Saya baru mulai belajar tentang redux dua hari yang lalu, jadi sejujurnya saya tidak tahu apa (jika ada) batasan yang ada sebagai akibat dari pendekatan ini. Di luar kepala saya, saya benar-benar tidak bisa memikirkan apa pun, dan semuanya tampaknya berfungsi dengan baik untuk kasus penggunaan saya, tetapi mungkin seseorang yang lebih berpengalaman dapat ikut campur.

Saya datang dengan pendekatan khusus ini karena saya sangat menyukai gagasan memiliki _truly_ komponen "bodoh" yang dapat memiliki penyedia yang ditugaskan untuk mereka. Ini menegakkan pemisahan masalah yang sebenarnya, dan memungkinkan sejumlah penyedia yang mudah dipertukarkan sebagai modul.

Selanjutnya, jika @gaearon menyukai pendekatan ini dan menganggapnya termasuk dalam cakupan react-redux , saya akan dengan senang hati mengirimkan PR dengan tambahan. Jika tidak, saya mungkin akan membuat dan menerbitkan react-redux-providers , dan akhirnya menerbitkan penyedia contoh (misalnya, hal-hal seperti react-redux-provide-toggle ) bersama dengan beberapa contoh dunia nyata.

Karena Anda dapat menghindari rendering ulang komponen tingkat menengah yang tidak perlu?

Ya

Arah yang telah kita ambil sekarang adalah bahwa kita akan menggunakan Immutable.js untuk status kita (Baik Status Aplikasi dan Status UI; kami menyimpan beberapa Status UI dalam komponen), dan kemudian menggunakan PureRenderMixin untuk semua komponen kami. Bukankah ini akan menghilangkan penalti kinerja menggunakan aliran top-down?

Sunting: Saya baru menyadari bahwa itu tidak akan menghilangkan penalti kinerja untuk komponen tingkat menengah jika salah satu komponen tingkat bawahnya masih perlu dirender ulang, tetapi masih harus menghilangkan banyak overhead. Ada pengalaman dengan itu?

@ danmaz74 Kami telah mengalami masalah kinerja dalam skenario seperti itu (menggunakan bukan redux, tetapi lib buatan rumah yang sangat mirip). Kami memiliki aplikasi yang cukup kompleks, dengan beberapa komponen yang sangat "mahal". Selain itu, saat aplikasi tumbuh lebih besar, memasukkan data di lebih banyak tempat daripada hanya di tingkat atas dapat membantu Anda menghindari pembuatan dependensi implisit antar komponen dan menghindari orang tua mengetahui terlalu banyak tentang persyaratan data anak-anak mereka.

@eldh terima kasih atas pembaruannya, itu masuk akal. Kami akan mengingatnya saat melanjutkan :)

Ada pembaruan tentang ini? contoh timbur sangat masuk akal bagi saya sementara latihan koneksi tunggal saya tidak begitu mengerti. Saya akan tertarik dengan argumen kontra, yaitu "orang memiliki preferensi yang berbeda di sini".

Menggunakan satu koneksi akan membutuhkan fungsi mapStateToProps yang sangat besar untuk mengubah status sepenuhnya menjadi misalnya hierarki dalam 10 komponen ... Saya agak bingung apa pemikiran di balik itu atau apakah saya salah memahami sesuatu ...

Menggunakan satu koneksi akan membutuhkan fungsi mapStateToProps yang besar

Tidak ada yang menganjurkan koneksi tunggal.

Kemudian, kita membungkus komponen yang ingin kita hubungkan ke Redux dengan fungsi connect() dari react-redux. Coba lakukan ini hanya untuk komponen tingkat atas, atau penangan rute . Meskipun secara teknis Anda dapat menghubungkan() komponen apa pun di aplikasi Anda ke Redux store, hindari melakukan ini terlalu dalam karena akan membuat aliran data lebih sulit untuk dilacak.

"Tunggal" hanya mengacu pada aplikasi kecil seperti yang kami buat dalam contoh. Silakan mengubah dokumen untuk memperjelas hal ini dengan lebih baik. Saya sekarang sibuk dengan proyek lain jadi tolong jangan berharap masalah ini akan bergerak kecuali seseorang membuat PR. Anda juga bisa melakukannya.

Terimakasih atas klarifikasinya.

Akhirnya sempat merilis react-redux-provide . Lihat di sini . Saya juga akan merilis beberapa hal lain dalam beberapa hari/minggu ke depan.

Itu terlihat sangat bagus, terima kasih!

Saya baru saja memulai proyek redux baru, dan saya menggunakan connect untuk komponen 'pintar' - ini jauh lebih masuk akal bagi saya, dan jika ada manfaat kinerja, ada kemenangan ekstra. Sebaliknya, jika Anda menggelembungkan semua kontrol ke aplikasi utama atau router, ada kehilangan SRP sepenuhnya - seberapa besar aplikasi Anda harus mendapatkan sebelum Anda mulai memecahnya?

Saya bahkan berpikir untuk mengatur elemen terkait ke dalam folder komponen - mis. letakkan peredam di samping komponen utamanya, dll.

Pada akhirnya, saya percaya bahwa redux/flux adalah manfaat besar untuk keadaan yang dapat diprediksi, tetapi perubahan mental seperti itu dari mv standar-apa pun yang telah membuat pengembangan aplikasi UI sederhana & dapat diakses oleh siapa saja, yang pada akhirnya fluks akan diabstraksikan dan kami akan pindah kembali ke sesuatu yang lebih mirip mv*.

Ini sedang diperbaiki di #1285.

Kami tidak lagi menyarankan untuk membuat komponen penampung di dokumen yang diperbarui.
http://redux.js.org/docs/basics/UsageWithReact.html

Hei saya menulis tentang beberapa hal yang mungkin membantu di sini. :)

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

Saya pikir https://github.com/reactjs/redux/issues/419#issuecomment -183769392 dapat membantu dengan #1353 juga.

@timbur Artikel yang luar biasa! Bisakah Anda juga membagikan pemikiran Anda tentang pertanyaan ini: https://github.com/reactjs/react-redux/issues/278

Saya tidak yakin apakah ini sangat jelas tetapi saya merasa perlu menambahkannya di sini untuk kejelasan, karena saya pikir banyak dari apa yang dikatakan mungkin agak abstrak bagi seseorang yang baru mengenal redux datang ke utas ini.

Ketika saya pertama kali mulai menggunakan redux, saya salah mengarahkan "wadah" (komponen yang terhubung) di dalam (cukup tua bodoh) "komponen" karena saya pikir aplikasi saya tidak akan memerlukan banyak decoupling. Betapa salahnya saya. Ketika saya menyadari bahwa saya perlu menggunakan kembali banyak komponen ini, saya harus melakukan sedikit refactoring dan memindahkan banyak barang pintar langsung ke atas pohon tetapi ini segera menjadi berat; "penyedia" di bagian atas menyediakan konteks (sebagaimana mestinya) tetapi juga pada dasarnya SEMUA aplikasi (yang seharusnya tidak).

Cara terbaik yang saya temukan untuk mendekatinya adalah dengan memiliki hierarki wadah yang menyusun komponen bodoh, dengan penyedia di bagian atas. Wadah hanya boleh hidup di dalam wadah lain . Aplikasi Anda harus berupa hierarki container yang menggunakan komponen untuk menyajikan datanya.

Seringkali cara yang baik untuk melakukan ini dengan daftar adalah dengan memberikan ID melalui komponen pintar. Kebutuhan akan ID adalah tanda bahwa sesuatu itu milik domain aplikasi. Jadi, jika memungkinkan, ambil daftar ID dalam satu wadah dan berikan ke wadah lain yang dapat menggunakan ID untuk mendapatkan informasi yang diinginkan. Di dalam setiap wadah, gunakan komponen untuk merender informasi itu tanpa memerlukan ID.

Di bawah ini saya telah mengolok-olok contoh (berbelit-belit) tentang cara meneruskan bagian aplikasi yang terhubung melalui hierarki wadah yang menggunakan komponen untuk menampilkannya.

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

Jadi di sini hierarki kontainer adalah TodoAppProvider > TodoListsContainer > TodoListContainer > TodoContainer . Mereka masing-masing dirender satu sama lain, tidak pernah dirender di dalam komponen, dan tidak mengandung kode tampilan mentah (terlepas dari div sesekali untuk alasan pembungkusan React).

Pada akhirnya, cara saya memikirkannya adalah seolah-olah Anda awalnya membuat pohon komponen terhubung yang memetakan data Anda ke dalam cara yang berguna yang _akan_ pedulikan pada UI Anda. Namun tidak ada UI sama sekali, hanya pohon status yang dipetakan melalui hierarki wadah yang adil . Setelah itu, Anda melewati dan menaburkan komponen presentasi di dalam wadah tersebut untuk benar-benar menampilkan data dengan cara apa pun yang Anda inginkan (yaitu di DOM). Jelas tidak berguna untuk menulis aplikasi Anda dengan cara dua arah ini, tetapi saya merasa berguna untuk membuat konsep model seperti ini.

Apakah ada penalti kinerja (atau alasan arsitektur lainnya) untuk menggunakan connect() lebih dari sekali? Saya mencoba menyediakan alat peraga yang sering digunakan untuk komponen dengan mengabstraksi titik masuk koneksi mereka dengan cara berikut:

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

Saya malas di sini dan tidak menggabungkan dua versi mapStateToProps dan dua versi mapDispatchToProps karena ini membuat deklarasi tetap sederhana. Tapi saya bertanya-tanya apakah itu ide yang buruk untuk membiarkan connect melakukan itu untuk saya.

@timotgl : Dan tidak lagi menjadi pengelola aktif - tolong jangan ping dia secara langsung.

Saya akan mengatakan bahwa pertanyaan Anda dijawab dalam entri FAQ Redux tentang menghubungkan beberapa komponen , tetapi sepertinya Anda bertanya tentang sesuatu yang berbeda - dengan sengaja membungkus beberapa koneksi di sekitar satu komponen? Saya tidak bisa mengatakan saya pernah melihat orang melakukan itu sebelumnya, dan saya telah melihat _lot_ kode Redux.

Secara pribadi, saya sarankan mencoba pendekatan yang berbeda. Entah memiliki HOC "alat peraga umum" atau sesuatu yang sebagian besar hanya meneruskannya ke anak-anaknya, atau menggunakan penyeleksi untuk mengambil alat peraga umum dalam fungsi mapState komponen tertentu dan menggabungkannya dengan alat peraga khusus yang diperlukan.

@markerikson Maaf tidak tahu itu, penyebutan dihapus.

Jadi pertama-tama, itu berfungsi, dan komponen itu muncul seperti komponen terhubung lainnya di alat pengembang reaksi, tidak memiliki pembungkus tambahan atau semacamnya.

Saya memutuskan untuk tidak menggunakan HOC karena saya tidak ingin melibatkan paradigma OOP/warisan, karena ini hanya tentang menyediakan lebih banyak alat peraga ke komponen, perilakunya tidak tersentuh.

Poin bagus tentang melakukan pengkabelan di mapStateToProps . Itu akan berhasil tetapi kemudian saya memiliki setidaknya 2 titik masuk - memanggil satu fungsi pembantu untuk terhubung tampaknya lebih mudah.

Tidak yakin apa yang Anda maksud dengan "dua titik masuk".

Yang saya gambarkan adalah seperti ini:

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 Dengan dua titik masuk yang saya maksud adalah Anda harus melakukan hal yang sama untuk mapDispatchToProps , dan berpotensi untuk mergeProps .

Anda seharusnya hampir tidak pernah menggunakan mergeProps - itu ada sebagai jalan keluar terakhir, dan kami tidak menyarankan penggunaannya. Saya juga secara umum menyarankan Anda untuk tidak benar-benar menulis fungsi mapDispatch , dan menggunakan "pendekatan objek" sebagai gantinya:

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

class TodoList extends Component {}

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

Anda dapat dengan mudah memiliki beberapa file index.js yang mengekspor ulang semua pembuat tindakan "umum" Anda, dan melakukan sesuatu seperti:

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

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

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

Anda seharusnya hampir tidak pernah menggunakan mergeProps - itu ada sebagai jalan keluar terakhir, dan kami tidak menyarankan penggunaannya.

Halo @markerikson , Saya hanya ingin tahu mengapa seseorang harus menghindari penggunaan mergeProps? Saya merasa sangat nyaman untuk "menyembunyikan" alat peraga dari mapStateToProps yang mungkin saya perlukan dalam tindakan saya di mapDispatchToProps tetapi tidak di komponen. Apakah itu hal yang buruk?

Apakah halaman ini membantu?
0 / 5 - 0 peringkat