U4EA
U4EA

Reputation: 912

React/Redux/Immutable - confusion regarding strategy for HOC with child component

I hope I can make sense of all of this in question format. I have been trying to work my way through this was the past 7 or so hours without success. My brain has simply run dry at this point and I am at a dead end regardless.

I am working with react 15.6.1, redux 3.7.1, react-redux 5.0.5, redux-immutable 4.0.0, redux-form 7.2.0, react-select 1.2.1.

My application has search functionality with field A and field B using 2 different forms (different forms for different pages). I don't it is pivotal to this question but I am using redux-form and react-select for the forms and search fields. I am storing user-entered search criteria in my Search reducer to synchronise autopopulation etc of select lists across the different forms.

redux-form---->react-select(field A) ---->react-select(field B)

My Search reducer is using Immutable.fromJs() in the initialState(). The reducer is working as expected. The problem I have is where to use the HOC in order to convert the Map object returned from the reducer into the JS array required for the react-select component.

MainSearchForm.js: -

import React, {Component} from 'react'
import { Field, reduxForm } from 'redux-form'
import { connect } from 'react-redux'

import FormFieldA from './FormFieldA'
import FormFieldB from './FormFieldB'

class MainSearchForm extends Component {

  render() {

    return(
    <form>
      <FormFieldA options={this.props.fieldAoptions}/>
      <FormFieldB options={this.props.fieldBoptions}/>
    </form>
    )
  }
}

function mapStateToProps ({Search}, props) {
  return {
    fieldAoptions: Search.get('fieldAoptions'),
    fieldBoptions: Search.get('fieldBoptions'),
  }
}

MainSearchForm = connect(mapStateToProps,{})(MainSearchForm);

export default reduxForm({
  form: 'main-search',
})(MainSearchForm)

To simply the example, both FormFieldA and FormFieldB components follow as the same: -

import React, {Component} from 'react'
import Select from 'react-select';

class FormFieldA extends Component {

  render() {

    const handleOnChange = (value) => {
      // call dispatch here
    }

    return(
      <Select
        name="field-a-input"
        id="field-a"
        options={this.props.options}
        onChange={handleOnChange}
      />
    )
  }
}

export default FormFieldA

So the options prop in the react-select component must be a JS array: -

options: [
    { label: 'Red' },
    { label: 'Green' },
    { label: 'Blue' }
]

I can convert this using Immutable.toJS() but the Redux official guide recommends against this for performance reasons, recommending this pattern using an (I assume reusable) HOC component to parse the Immutable Map to the JS array.

My question is, how would I incorporate this? As you can see in the code above, right now my MainSearchForm is connecting to the Redux store to retrieve the options data required as options for the react-select options props. Would the answer be to just not have MainSearchForm and instead have a intermediary component for each field rendered by MainSearchForm, with this intermediary component call the HOC before using connect, as is seen in the guide: -

HOC from the Redux Immutable guide: -

import React from 'react'
import { Iterable } from 'immutable'

export const toJS = WrappedComponent => wrappedComponentProps => {
  const KEY = 0
  const VALUE = 1

  const propsJS = Object.entries(
    wrappedComponentProps
  ).reduce((newProps, wrappedComponentProp) => {
    newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(
      wrappedComponentProp[VALUE]
    )
      ? wrappedComponentProp[VALUE].toJS()
      : wrappedComponentProp[VALUE]
    return newProps
  }, {})

  return <WrappedComponent {...propsJS} />
}

Example intermediary smart component to parse the Immutable Map through the HOC and connect() with FormFieldA: -

import { connect } from 'react-redux'
import { toJS } from './to-js'
import FormFieldA from './FormFieldA'

    function mapStateToProps ({Search}, props) {
      return {
        fieldAoptions: Search.get('fieldAoptions'),
      }
    }
export default connect(mapStateToProps)(toJS(FormFieldA))

Would that be best practice?

I would sincerely appreciate any help with this. This is my first time working with HOC & Immutable and it's a lot to take in. But I think I have finally got a grasp of the paradigm.

Upvotes: 3

Views: 452

Answers (1)

Fabio Antunes
Fabio Antunes

Reputation: 22862

Don't worry too much about best practises, the best practises of today are the anti patterns of tomorrow. Don't get me wrong is good to know what's the "best" way doing things, but the best is always relative so take that into consideration.

The idea is that your FormFieldA and FormFieldB shouldn't care about redux, immutable, foobar, bajouras, wtvLibraryOrToolThatMightCome.

So your HOC, for the immutable should be in, your MainSearchForm:

import React, {Component} from 'react'
import { Field, reduxForm } from 'redux-form'
import { connect } from 'react-redux'

import { toJS } from './to-js'

import FormFieldA from './FormFieldA'
import FormFieldB from './FormFieldB'

class MainSearchForm extends Component {

  render() {

    return(
    <form>
      <FormFieldA options={this.props.fieldAoptions}/>
      <FormFieldB options={this.props.fieldBoptions}/>
    </form>
    )
  }
}

function mapStateToProps ({Search}, props) {
  return {
    fieldAoptions: Search.get('fieldAoptions'),
    fieldBoptions: Search.get('fieldBoptions'),
  }
}

MainSearchForm = connect(mapStateToProps,{})(toJS(MainSearchForm));

export default reduxForm({
  form: 'main-search',
})(MainSearchForm)

Leave your FormFieldA and FormFieldB as they are, very dumb and just waiting for a simple array of options.

This is just a personal taste, but I usually even separate the component from the redux stuff.

For example with your case I would have MainSearchForm and a MainSearchFormContainer

import React, {Component} from 'react'
import FormFieldA from './FormFieldA'
import FormFieldB from './FormFieldB'

class MainSearchForm extends Component {

  render() {

    return(
    <form>
      <FormFieldA options={this.props.fieldAoptions}/>
      <FormFieldB options={this.props.fieldBoptions}/>
    </form>
    )
  }
}

export default MainSearchForm

And the container:

import { Field, reduxForm } from 'redux-form'
import { connect } from 'react-redux'
import MainSearchForm from './MainSearchForm'
import { toJS } from './to-js'

function mapStateToProps ({Search}, props) {
  return {
    fieldAoptions: Search.get('fieldAoptions'),
    fieldBoptions: Search.get('fieldBoptions'),
  }
}

const MainSearchFormContainer = connect(mapStateToProps,{})(toJS(MainSearchForm));

export default reduxForm({
  form: 'main-search',
})(MainSearchFormContainer)

Upvotes: 4

Related Questions