Search code examples
javascriptreactjsreact-reduxredux-form

Access prop outside of class in React when calling Higher Order Component


I am trying to use a Higher Order Component(HOC) pattern to reuse some code that connects to state and uses the Redux Form formValueSelector method.

formValueSelector requires a sting referencing the name of the form. I would like to set this dynamically and be able to use this HOC whenever I need the values of items. I use the item values to make calculations.

In the code below the HOC is passed the component and a string. I would like to set this to the prop formName that has been passed in from the parent(form).

I am new to the HOC pattern so any tips would be most appreciated.

HOC

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { formValueSelector } from 'redux-form';

function FormItemsValueSelectorHOC(FormElement, formName) {
  const selector = formValueSelector(formName);
  @connect(state => {
    console.log(state);
    const items = selector(state, 'items');
    return {
      items
    };
  }, null)
  class Base extends Component {
    render() {
      return (
        <FormElement {...this.props} />
      );
    }
  }
  return Base;
}
export default FormItemsValueSelectorHOC;

Wrapped Component

import React, { Component, PropTypes } from 'react';
import { Field } from 'redux-form';
import formItemsValueSelectorHOC from '../Utilities/FormItemsValueSelectorHOC';

const renderField = ({ placeholder, input, type}) => {
  return (
    <input
      {...input}
      placeholder={placeholder}
      type={type}
    />
  );
};

class StatementLineItemDesktop extends Component {
  static propTypes = {
    items: PropTypes.array.isRequired,
    index: PropTypes.number.isRequired,
    item: PropTypes.string.isRequired,
    fields: PropTypes.object.isRequired,
    formName: PropTypes.string.isRequired
  };

  calculateLineTotal(items, index) {
    let unitPrice = '0';
    let quantity = '0';
    let lineTotal = '0.00';
    if (items) {
      if (items[index].price) {
        unitPrice = items[index].price.amountInCents;
      }
      quantity = items[index].quantity;
    }
    if (unitPrice && quantity) {
      lineTotal = unitPrice * quantity;
      lineTotal = Number(Math.round(lineTotal+'e2')+'e-2').toFixed(2); 
    }
    return <input value={lineTotal} readOnly placeholder="0.00" />;
  }

  render() {
    const { items, index, item, fields, formName} = this.props;
    return (
      <tr id={`item-row-${index}`} key={index} className="desktop-only">
        <td>
          <Field
            name={`${item}.text`}
            type="text"
            component={renderField}
            placeholder="Description"
          />
        </td>
        <td>
          <Field
            name={`${item}.quantity`}
            type="text"
            component={renderField}
            placeholder="0.00"
          />
        </td>
        <td>
          <Field
            name={`${item}.price.amountInCents`}
            type="text"
            component={renderField}
            placeholder="0.00"
          />
        </td>
        <td className="last-col">
          <Field
            name={`${item}.price.taxInclusive`}
            type="hidden"
            component="input"
          />
          {::this.calculateLineTotal(items, index)}
          <a
            className="remove-icon"
            onClick={() => fields.remove(index)}
          >
            <span className="icon icon-bridge_close" />
          </a>
        </td>
      </tr>
    );
  }
}

export default formItemsValueSelectorHOC(StatementLineItemDesktop, 'editQuote');

Solution

  • TLDR: use ownProps parameter

    Draft of what you should do

    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { formValueSelector } from 'redux-form';
    
    function FormItemsValueSelectorHOC(FormElement) {
      @connect((state, ownProps) => {
        const formName = ownProps.formName;
        const selector = formValueSelector(formName);
        const items = selector(state, 'items');
        return {
          items
        };
      }, null)
      class Base extends Component {
        render() {
          // Now in here you should omit `formName` from the props you are
          // passing to your Form Element since it's not used overthere
          return (
            <FormElement {...this.props} />
          );
        }
      }
      return Base;
    }
    export default FormItemsValueSelectorHOC;
    

    And this will be the way you will create your connected component

    formItemsValueSelectorHOC(StatementLineItemDesktop);
    

    And this will be the way you use it

    <ConnectedStatementLineItemDesktop formName={"editQuote"} />
    

    Let me explain a bit more how this work

    The thing that you were missing is React-Redux API, you should probably explore it more because it already contemplates a lot of this use cases

    So, React-Redux's connect function's first parameter is called mapStateToProps.

    This is its signature:

    mapStateToProps(state, [ownProps]): stateProps
    

    The thing that I want to remark is the ownProps parameter.

    ownProps contains all the props that are passed to your connected component.

    Just for clarification, you have these components

    • Regular Component: i.e. StatementLineItemDesktop and Base
    • Connected Component: i.e. ConnectedBase = connect(mapStateToProps)(Base)

    So, from this information, your HOC function called FormItemsValueSelectorHOC returns a variation of ConnectedBase.

    So, whatever props you pass to ConnectedBase or whatever component it's returned from FormItemsValueSelectorHOC you can access them from ownProps

    BTW, in your particular case, this is your mapStateToProps

    function mapStateToProps(state, ownProps) {
      const formName = ownProps.formName;
      const selector = formValueSelector(formName);
      const items = selector(state, 'items');
      return {
        items
      };
    }
    

    The thing to Note is that connect is also an HOC, so it's logically to think that most of the things you do with normal components you will also be able to do with connected Components, I would suggest for you to read the connect source, it's not long nor difficult so you can probably explore and comprehend more of this answer.

    I hope this helped.