React-Redux - Reuseable Container/Connector

2019-09-05 23:09发布

问题:

I am completely lost on the react-redux container (ie connector) concept as it is not doing what I anticipated. My issue is straight forward, and to me reasonable, yet I cannot find a well written example of how to accomplish it.

Let us say we have a react component that connects to a store that has product context, and we will call this component ProductContext.

Furthermore, let's say we want to reuse ProductContext liberally throughout the app so as to avoid the boilerplate code of dispatching actions on every other component that may need products.

Illustratively this is what I mean:

from DiscountuedProducts:

<ProductContext >
   // do something with props from container
</ProductContext >

from SeasonalProducts:

<ProductContext >
   // do something with props from container
</ProductContext >

From the examples I see at react-redux, it appears to me that their containers lump both seasonal and discontinued products in the container itself. How is that reusable?

from the ProductContextComponent:

<section >
  <DiscontinuedProducts />
  <SeasonalProducts />
</section >

Complicating matters, while trying to keep a cool head about this most frustrating matter, "nebulous tersity" seems to be the only responses I receive.

So here is my ProductContext:

@connect(state => ({
   products: state.productsReducer.products
}))
export default class ProductContext extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    const { dispatch } = this.props;
    const clientId = this.context.clientInfo._id;
    dispatch(fetchProductsIfNeeded(clientId));
  }

 // get from parent
 static  contextTypes = {
    clientInfo: PropTypes.object.isRequired
  };

  static propTypes = {
    children: PropTypes.node.isRequired,
    dispatch: PropTypes.func.isRequired,
    products: PropTypes.array
 };


  render() {
    if (!this.props.products) return <Plugins.Loading />;
    .....
    return (
         // I want to put the products array here
      );
    }
 };

Then my thinking is if I do this:

from DiscountuedProducts:

<ProductContext >
   // do something with props from container
</ProductContext >

DiscontinuedProducts should have knowledge of those products and it can simply filter for what is discontinued.

Am I completely wrong on this? Is this not reasonable to desire?

If anyone knows of a exhaustive example on the Net demonstrating how this can be achieved, I would much appreciate it being pointed out to me. I have spent over a week on this issue and am about ready to give up on react-redux.

UPDATE: A very slick solution below with use of a HOC.

回答1:

If anyone knows of a exhaustive example on the Net demonstrating how this can be achieved, I would much appreciate it being pointed out to me.

Look at the Shopping Cart example.

Examples code from: shopping-cart/containers/CartContainer.js

Let us say we have a react component that connects to a store that has product context,

There isn't a store for products and a store for users, etc. There is one store for everything. You should use reducers to take the full store and reduce to what your component needs.

From the example:

import { getTotal, getCartProducts } from '../reducers'

const mapStateToProps = (state) => {
  return {
    products: getCartProducts(state),
    total: getTotal(state)
  }
}

Here state is the entire store.

let's say we want to reuse ProductContext liberally throughout the app so as to avoid the boilerplate code of dispatching actions

Components don't dispatch actions, they call functions passed to them. The functions live in an actions module, which you import and pass to container as props. In turn, the container passes those functions to component props.

From the example:

import { checkout } from '../actions'

CartContainer.propTypes = {
  checkout: PropTypes.func.isRequired
}

connect(mapStateToProps,
  { checkout }
)(CartContainer)

What connect does, it subscribes to store changes, calls the map function, merges with constant props (here the action function), and assigns new props to the container.

DiscontinuedProducts should have knowledge of those products and it can simply filter for what is discontinued

This is actually spot on. The knowledge you mention is the one store, and it should absolutely filter in a reducer.

Hopefully this clears things up.



回答2:

I have found a more practical way of utilizing repetitive data from redux than the docs. It makes no sense to me to repeat mapToProps and dispatch instantiation on every blessed component when it was already done once at a higher level, and there inlies the solution. Make sure your app is Babel 6 compliant as I used decorators.

1. I created a higher order component for the context ....

product-context.js:

   import React, { Component, PropTypes } from 'react';
   // redux
   import { connect } from 'react-redux';
   import { fetchProductsIfNeeded }  from '../../redux/actions/products-actions';

    // productsReducer is already in state from index.js w/configureStore
    @connect(state => ({
      products: state.productsReducer.products
    }))
   export default function ProductContext(Comp) {
    return (
      class extends Component {
        static propTypes = {
          children: PropTypes.node,
          dispatch: PropTypes.func.isRequired,
          products: PropTypes.array
        };

        static contextTypes = {
         clientInfo: PropTypes.object.isRequired;
        };

        componentDidMount() {
          const { dispatch } = this.props;
          const clientId = this.context.clientInfo._id;
          dispatch(fetchProductsIfNeeded(clientId));
        }

        render() {
          if (!this.props.products) return (<div>Loading products ..</div>);
          return (
            <Comp products={ this.props.products }>
              { this.props.children }
            </Comp>
          )
        }
      }
    )
  }

2. component utilizing product-context.js

carousel-slider.js:

import React, { Component, PropTypes } from 'react';
......
import ProductContext from '../../../context/product-context';

@Radium
@ProductContext 
export default class CarouselSlider extends Component {

  constructor(props) {
    super(props);   }

  ......

  static showSlideShow(carouselSlides) {
    carouselSlides.map((slide, index) => {
       ......          
     results.push (
       ......
      )
    });
    return results;   
  }

  render() {
    const carouselSlides = this.props.products;
    const results = CarouselSlider.showSlideShow(carouselSlides);
    return (
      <div id="Carousel" className="animation" ref="Carousel">
        { results }
      </div>
    );   
  } 
}

So there you go. All I needed was a decorator reference to product-context, and as a HOC, it returns the carousel component back with the products prop.

I saved myself at least 10 lines of repetitive code and I removed all related contextType from lower components as it is no longer needed with use of the decorator.

Hope this real world example helps as I detest todo examples.