erikras/redux-form

View on GitHub
src/ConnectedFieldArray.js

Summary

Maintainability
A
2 hrs
Test Coverage
// @flow
import React, { Component, createElement } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import createFieldArrayProps from './createFieldArrayProps'
import { mapValues } from 'lodash'
import plain from './structure/plain'
import type { ElementRef } from 'react'
import type { Structure } from './types'
import type { Props, DefaultProps } from './ConnectedFieldArray.types'
import validateComponentProp from './util/validateComponentProp'

const propsToNotUpdateFor = ['_reduxForm', 'value']

export default function createConnectedFieldArray(structure: Structure<any, any>) {
  const { deepEqual, getIn, size, equals, orderChanged } = structure
  const getSyncError = (syncErrors: Object, name: string) => {
    // For an array, the error can _ONLY_ be under _error.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return plain.getIn(syncErrors, `${name}._error`)
  }

  const getSyncWarning = (syncWarnings: Object, name: string) => {
    // For an array, the warning can _ONLY_ be under _warning.
    // This is why this getSyncError is not the same as the
    // one in Field.
    return getIn(syncWarnings, `${name}._warning`)
  }

  class ConnectedFieldArray extends Component<Props> {
    static defaultProps: DefaultProps
    ref: ElementRef<any> = React.createRef()

    shouldComponentUpdate(nextProps: Props) {
      // Update if the elements of the value array was updated.
      const thisValue: any = this.props.value
      const nextValue: any = nextProps.value

      if (thisValue && nextValue) {
        const nextValueItemsSame = equals(nextValue, thisValue) //.every(val => ~thisValue.indexOf(val))
        const nextValueItemsOrderChanged = orderChanged(thisValue, nextValue)
        const thisValueLength = thisValue.length || thisValue.size
        const nextValueLength = nextValue.length || nextValue.size

        if (
          thisValueLength !== nextValueLength ||
          (nextValueItemsSame && nextValueItemsOrderChanged) ||
          (nextProps.rerenderOnEveryChange &&
            thisValue.some((val, index) => !deepEqual(val, nextValue[index])))
        ) {
          return true
        }
      }

      const nextPropsKeys = Object.keys(nextProps)
      const thisPropsKeys = Object.keys(this.props)
      // if we have children, we MUST update in React 16
      // https://twitter.com/erikras/status/915866544558788608
      return !!(
        this.props.children ||
        nextProps.children ||
        nextPropsKeys.length !== thisPropsKeys.length ||
        nextPropsKeys.some(prop => {
          // useful to debug rerenders
          // if (!plain.deepEqual(this.props[ prop ], nextProps[ prop ])) {
          //   console.info(prop, 'changed', this.props[ prop ], '==>', nextProps[ prop ])
          // }
          return (
            !~propsToNotUpdateFor.indexOf(prop) && !deepEqual(this.props[prop], nextProps[prop])
          )
        })
      )
    }

    get dirty(): boolean {
      return this.props.dirty
    }

    get pristine(): boolean {
      return this.props.pristine
    }

    get value(): any {
      return this.props.value
    }

    getRenderedComponent() {
      return this.ref.current
    }

    getValue = (index: number): any => this.props.value && getIn(this.props.value, String(index))

    render() {
      const {
        component,
        forwardRef,
        name,
        _reduxForm, // eslint-disable-line no-unused-vars
        validate, // eslint-disable-line no-unused-vars
        warn, // eslint-disable-line no-unused-vars
        rerenderOnEveryChange, // eslint-disable-line no-unused-vars
        ...rest
      } = this.props
      const props = createFieldArrayProps(
        structure,
        name,
        _reduxForm.form,
        _reduxForm.sectionPrefix,
        this.getValue,
        rest
      )
      if (forwardRef) {
        props.ref = this.ref
      }
      return createElement(component, props)
    }
  }

  ConnectedFieldArray.propTypes = {
    component: validateComponentProp,
    props: PropTypes.object,
    rerenderOnEveryChange: PropTypes.bool
  }

  ConnectedFieldArray.defaultProps = {
    rerenderOnEveryChange: false
  }

  const connector = connect(
    (state, ownProps) => {
      const {
        name,
        _reduxForm: { initialValues, getFormState }
      } = ownProps
      const formState = getFormState(state)
      const initial =
        getIn(formState, `initial.${name}`) || (initialValues && getIn(initialValues, name))
      const value = getIn(formState, `values.${name}`)
      const submitting = getIn(formState, 'submitting')
      const syncError = getSyncError(getIn(formState, 'syncErrors'), name)
      const syncWarning = getSyncWarning(getIn(formState, 'syncWarnings'), name)
      const pristine = deepEqual(value, initial)
      return {
        asyncError: getIn(formState, `asyncErrors.${name}._error`),
        dirty: !pristine,
        pristine,
        state: getIn(formState, `fields.${name}`),
        submitError: getIn(formState, `submitErrors.${name}._error`),
        submitFailed: getIn(formState, 'submitFailed'),
        submitting,
        syncError,
        syncWarning,
        value,
        length: size(value)
      }
    },
    (dispatch, ownProps) => {
      const { name, _reduxForm } = ownProps
      const {
        arrayInsert,
        arrayMove,
        arrayPop,
        arrayPush,
        arrayRemove,
        arrayRemoveAll,
        arrayShift,
        arraySplice,
        arraySwap,
        arrayUnshift
      } = _reduxForm
      return mapValues(
        {
          arrayInsert,
          arrayMove,
          arrayPop,
          arrayPush,
          arrayRemove,
          arrayRemoveAll,
          arrayShift,
          arraySplice,
          arraySwap,
          arrayUnshift
        },
        actionCreator => bindActionCreators(actionCreator.bind(null, name), dispatch)
      )
    },
    undefined,
    { forwardRef: true }
  )
  return connector(ConnectedFieldArray)
}