title: Formik - Plug custom validation for custom component into my currently working Formik form
body: <p>I've got a form with some text inputs, and some custom components. I've got the Formik validation working on the text inputs but not the custom components. I am now trying to add Formik validation to my custom <code>categoriesMultiselect</code> component. This component holds its data in the redux store. I have handled the validation myself and added a value to my redux props: </p>
  
  <pre><code>const mapStateToProps = (
    state: RecordOf&lt;VepoState&gt;,
    ownProps: { rerenderKey: boolean }
  ) =&gt; ({...
      isCategoriesValid: selectIsCategoriesValid(state),
  ...
  })
  </code></pre>
  
  <p>. I just want to plug <code>props.isCategoriesValid</code> into my Formik form. </p>
  
  <p>From reading the official documentation, I think I do that by adding <code>validate={validateCategories}</code> as a prop to the custom component and adding the function <code>validateCategories</code> to the component. So I have tried that like this:</p>
  
  <pre><code>//above React render()
        validateCategories = () =&gt; {
          let error
          if (!this.props.selectIsCategoriesValid) {
            error = 'please select a category'
          }
          return error
        }
  
  // Inside React render()
            &lt;CategoriesMultiselect.View
              validate={this.validateCategories}
              name={'categories'}
              label={'Product Categories'}
              categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
            /&gt;
  </code></pre>
  
  <p><code>validateCategories</code> never gets run. That's why I have tested running it by adding <code>validateField</code> to one of my inputs <code>onChange</code> function:</p>
  
  <pre><code>      &lt;Input
          label={'Product Brand'}
          value={values.brand}
          onTouch={setFieldTouched}
          error={touched.brand &amp;&amp; errors.brand}
          placeholder="Enter Brand"
          name="brand"
          required
          onChange={() =&gt; validateField('categories')}
          deleteText={setFieldValue}
        /&gt;
  </code></pre>
  
  <p>When it tries to validate the field, I get console error:</p>
  
  <blockquote>
    <p>Cannot read property 'validate' of undefined</p>
  </blockquote>
  
  <p>on this line of code in Formik:</p>
  
  <pre><code>  var validateField = useEventCallback(function (name) {
  if (isFunction(fieldRegistry.current[name].validate)) {
  </code></pre>
  
  <p>I have at least plugged Formik into my Redux because I am at least successfully dispatching a Redux action when submitting the form. What am I doing wrong?</p>
  
  <p>Code:</p>
  
  <pre><code>//@flow
  
  import * as Yup from 'yup'
  import { Formik, withFormik } from 'formik'
  import { Container } from 'native-base'
  import * as React from 'react'
  import { ScrollView, View, Alert, Button } from 'react-native'
  import { connect } from 'react-redux'
  import { Category as CategoryEnums } from 'src/enums'
  import type { VepoState } from 'src/components/model'
  import type { RecordOf } from 'immutable'
  import type { Product } from 'src/model'
  import VepoHeader from 'src/components/formControls/header/view'
  import { selectIsAddFormValid } from './selector'
  import { selectProduct } from './selector'
  // import { Button } from 'src/components/formControls'
  import { ImagePicker } from 'src/components/formControls'
  import LocationAutocomplete from 'src/components/formControls/locationAutocomplete/view'
  import { uploadAddProduct, updateRerenderKey } from './action'
  import { viewStyle } from './style'
  import type { Dispatch } from 'redux'
  import { updateAddProductImage } from './action'
  import type { Place } from 'src/model/location'
  import { Colors, Spacing } from 'src/styles'
  import { Input } from 'src/components/formControls'
  import { onPress } from './controller'
  import { CategoriesMultiselect } from 'src/components/formControls'
  import {
    selectIsGrocerySelected,
    selectIsCategoriesValid,
    isLocationValid
  } from 'src/components/product/add/groceryItem/selector'
  
  const mapStateToProps = (
    state: RecordOf&lt;VepoState&gt;,
    ownProps: { rerenderKey: boolean }
  ) =&gt; ({
    locationListDisplayed: state.formControls.root.locationListDisplayed,
    isAddFormValid: selectIsAddFormValid(state),
    // $FlowFixMe
    product: selectProduct(state),
    // $FlowFixMe
    isGrocerySelected: selectIsGrocerySelected(state),
    // $FlowFixMe
    categories: state.formControls.categories,
    isCategoriesValid: selectIsCategoriesValid(state),
    image: state.product.add.image,
    rerenderKey: ownProps.rerenderKey,
    location: state.formControls.location,
    isLocationValid: isLocationValid(state)
  })
  
  // eslint-disable-next-line flowtype/no-weak-types
  const mapDispatchToProps = (dispatch: Dispatch&lt;*&gt;): Object =&gt; ({
    updateAddProductImage: (value): void =&gt; {
      dispatch(updateAddProductImage({ value }))
    },
    uploadAddProduct: (product: Product): void =&gt; {
      dispatch(uploadAddProduct(product))
    },
    updateRerenderKey: () =&gt; {
      dispatch(updateRerenderKey())
    }
  })
  
  export const getLocationIsValid = (place: Place): boolean =&gt; {
    return Object.keys(place).length &gt; 0 ? true : false
  }
  type AddGroceryStoreState = {
    name: string,
    brand: string,
    description: string,
    price?: number
  }
  
  class AddGroceryItemView extends React.Component&lt;any, AddGroceryStoreState&gt; {
    validateCategories = () =&gt; {
      let error
      if (!this.props.selectIsCategoriesValid) {
        error = 'please select a category'
      }
      return error
    }
    render() {
      const {
        values,
        handleSubmit,
        setFieldValue,
        errors,
        touched,
        setFieldTouched,
        isValid,
        isSubmitting,
        validateField
      } = this.props
      return (
        &lt;Container&gt;
          &lt;VepoHeader title={'Add Vegan Grocery Product'} /&gt;
          &lt;Container style={container}&gt;
            &lt;ScrollView
              keyboardShouldPersistTaps="always"
              style={viewStyle(this.props.locationListDisplayed).scrollView}&gt;
              &lt;View&gt;
                &lt;LocationAutocomplete
                  label={'Grocery Store'}
                  placeHolder={'Enter Grocery Store'}
                /&gt;
              &lt;/View&gt;
              &lt;View style={viewStyle().detailsContainer}&gt;
                &lt;ImagePicker
                  label={'Product Image (optional)'}
                  image={this.props.image.image}
                  updateAddProductImage={this.props.updateAddProductImage}
                  updateRerenderKey={this.props.updateRerenderKey}
                /&gt;
                &lt;Input
                  label={'Product Name'}
                  onTouch={setFieldTouched}
                  value={values.name}
                  placeholder="Enter Name"
                  name="name"
                  required
                  error={touched.name &amp;&amp; errors.name}
                  deleteText={setFieldValue}
                  onChange={setFieldValue}
                /&gt;
                &lt;Input
                  label={'Product Brand'}
                  value={values.brand}
                  onTouch={setFieldTouched}
                  error={touched.brand &amp;&amp; errors.brand}
                  placeholder="Enter Brand"
                  name="brand"
                  required
                  onChange={() =&gt; validateField('categories')}
                  deleteText={setFieldValue}
                /&gt;
                &lt;View&gt;
                  &lt;Input
                    label={'Product Description'}
                    value={values.description}
                    placeholder="Enter Description"
                    multiline={true}
                    required
                    onTouch={setFieldTouched}
                    error={touched.description &amp;&amp; errors.description}
                    numberOfLines={4}
                    name="description"
                    deleteText={setFieldValue}
                    onChange={setFieldValue}
                  /&gt;
                  &lt;Input
                    isValid={true}
                    isPrice={true}
                    label={'Product Price'}
                    value={values.price}
                    onTouch={setFieldTouched}
                    error={touched.price &amp;&amp; errors.price}
                    placeholder="Enter Price"
                    name="price"
                    deleteText={setFieldValue}
                    onChange={setFieldValue}
                  /&gt;
                  &lt;View&gt;
                    &lt;CategoriesMultiselect.View
                      validate={this.validateCategories}
                      name={'categories'}
                      label={'Product Categories'}
                      categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
                    /&gt;
                  &lt;/View&gt;
                &lt;/View&gt;
              &lt;/View&gt;
            &lt;/ScrollView&gt;
          &lt;/Container&gt;
          &lt;Button
            title="submit"
            onPress={handleSubmit}
            disabled={!isValid || isSubmitting}
            loading={isSubmitting}
          /&gt;
          {/* &lt;Button.View onSub={this._handleSubmit} onPress={this._handleSubmit} label={'GO!'} /&gt; */}
        &lt;/Container&gt;
      )
    }
  }
  
  const container = {
    flex: 1,
    ...Spacing.horizontalPaddingLarge,
    backgroundColor: Colors.greyLight,
    flexDirection: 'column'
  }
  
  const formikEnhancer = withFormik({
    validationSchema: Yup.object().shape({
      name: Yup.string().required(),
      brand: Yup.string().required(),
      categories: Yup.array(),
      description: Yup.string()
        .min(9)
        .required(),
      price: Yup.number()
        .typeError('price must be a number')
        .required()
    }),
    mapPropsToValues: () =&gt; ({
      name: '',
      brand: '',
      description: '',
      price: '',
      categories: []
    }),
    handleSubmit: (values, { props }) =&gt; {
      props.updateRerenderKey()
    },
    displayName: 'AddGroceryItemView'
  })(AddGroceryItemView)
  
  // $FlowFixMe
  const AddGroceryItemViewComponent = connect(
    mapStateToProps,
    mapDispatchToProps
  )(formikEnhancer)
  
  export default AddGroceryItemViewComponent
  </code></pre>
  
  <p>As Rikin requested, here is the CategoriesMultiselect component:</p>
  
  <pre><code>//@flow
  import type { Node } from 'react'
  import { selectSelectedCategory } from 'src/components/product/add/groceryItem/selector'
  import type { VepoState } from 'src/components/model'
  import type { RecordOf } from 'immutable'
  import { connect } from 'react-redux'
  import * as React from 'react'
  import { View } from 'react-native'
  import {
    List,
    ListItem,
    Text,
    Left,
    Body,
    Right,
    Button,
    Container,
    Label,
    Title,
    Content
  } from 'native-base'
  import Icon from 'react-native-vector-icons/FontAwesome'
  import Eicon from 'react-native-vector-icons/EvilIcons'
  import Modal from 'react-native-modal'
  import SelectMultiple from 'react-native-select-multiple'
  import {
    updateAlertModalIsOpen,
    updateAlertModalHasYesNo,
    updateAlertModalMessage,
    updateAlertModalTitle
  } from 'src/components/formControls/alertModal/action'
  import * as C from './model'
  import type { Subcategory } from 'src/model/category'
  
  import * as controller from './controller'
  import { getIsCategoriesValid } from './controller'
  import { styles } from 'src/components/style'
  import {
    Colors,
    Corners,
    Distances,
    Modals,
    Spacing,
    Typography,
    ZIndexes
  } from 'src/styles'
  import { Containers } from '../../../styles'
  import {
    toggleSubcategory,
    setAllShowSubcategoriesToFalse,
    toggleShowSubcategories
  } from './action'
  import type { Dispatch } from 'redux'
  
  const mapStateToProps = (state: RecordOf&lt;VepoState&gt;) =&gt; ({
    vepo: state,
    // $FlowFixMe
    selectedCategory: selectSelectedCategory(state),
    categories: state.formControls.categories
  })
  
  // eslint-disable-next-line flowtype/no-weak-types
  const mapDispatchToProps = (dispatch: Dispatch&lt;*&gt;): Object =&gt; ({
    setAllShowSubcategoriesToFalse: (): void =&gt; {
      dispatch(setAllShowSubcategoriesToFalse())
    },
    toggleSubcategory: (sc): void =&gt; {
      return dispatch(toggleSubcategory(sc))
    },
    toggleShowSubcategories: (c): void =&gt; {
      dispatch(toggleShowSubcategories(c))
    },
    updateAlertModalIsOpen: (isOpen: boolean): void =&gt; {
      dispatch(updateAlertModalIsOpen(isOpen))
    },
    updateAlertModalMessage: (message: string): void =&gt; {
      dispatch(updateAlertModalMessage(message))
    },
    updateAlertModalHasYesNo: (hasYesNo: boolean): void =&gt; {
      dispatch(updateAlertModalHasYesNo(hasYesNo))
    },
    updateAlertModalTitle: (title: string): void =&gt; {
      dispatch(updateAlertModalTitle(title))
    }
  })
  
  const renderCategoryRow = (props: C.CategoriesViewProps, item: C.Category) =&gt; {
    return (
      &lt;View&gt;
        &lt;ListItem
          style={listItem}
          icon
          onPress={() =&gt; controller.categoryClicked(props, item)}&gt;
          &lt;Left&gt;
            &lt;Icon
              style={styles.icon}
              name={item.icon}
              size={20}
              color={item.iconColor}
            /&gt;
          &lt;/Left&gt;
          &lt;Body style={[styles.formElementHeight, border(item)]}&gt;
            &lt;Text style={Typography.brownLabel}&gt;{item.label}&lt;/Text&gt;
          &lt;/Body&gt;
          &lt;Right style={[styles.formElementHeight, border(item)]}&gt;
            &lt;Eicon style={catStyle.arrow} name="chevron-right" size={30} /&gt;
          &lt;/Right&gt;
        &lt;/ListItem&gt;
      &lt;/View&gt;
    )
  }
  const getCategoriesToDisplay = (props) =&gt; {
    const y = props.categories.filter((x) =&gt; props.categoryCodes.includes(x.code))
    return y
  }
  
  class CategoriesMultiselectView extends React.Component {
    setFormCategories = () =&gt; {
      if (this.props &amp;&amp; this.props.setFieldValue) {
        this.props.setFieldValue(
          'categories',
          controller.getSelectedSubcategories(this.props.categories)
        )
      }
    }
  
    render(): React.Node {
      const categoriesToDisplay = getCategoriesToDisplay(this.props)
      return (
        &lt;View&gt;
          &lt;View style={{ ...Containers.fullWidthRow }}&gt;
            &lt;Label disabled={false} style={Typography.formLabel}&gt;
              {this.props.label}
            &lt;/Label&gt;
            &lt;View style={{ ...Containers.fullWidthRow }} /&gt;
            &lt;Label disabled={false} style={Typography.formLabel}&gt;
              {controller.getNumberOfSelectedSubcategories(this.props.categories)}{' '}
              Selected
            &lt;/Label&gt;
          &lt;/View&gt;
          &lt;View
            style={catStyle.categoriesViewStyle(this.props, categoriesToDisplay)}&gt;
            {this.props.categories &amp;&amp; this.props.categories.length &gt; 0 &amp;&amp; (
              &lt;List
                listBorderColor={'white'}
                style={categoriesListStyle}
                dataArray={categoriesToDisplay}
                renderRow={(item: C.Category) =&gt; {
                  return renderCategoryRow(this.props, item)
                }}
              /&gt;
            )}
            &lt;View style={catStyle.modalConatinerStyle} /&gt;
            &lt;Modal
              style={catStyle.modal}
              onModalHide={this.setFormCategories}
              isVisible={
                this.props.categories
                  ? this.props.categories.some((cat: C.Category) =&gt;
                      controller.showModal(cat)
                    )
                  : false
              }&gt;
              &lt;Container style={catStyle.modalView}&gt;
                &lt;View style={Modals.modalHeader}&gt;
                  &lt;Title style={catStyle.categoriesTitleStyle}&gt;
                    {controller.getDisplayedCategoryLabel(this.props.categories)}
                  &lt;/Title&gt;
                  &lt;Right&gt;
                    &lt;Button
                      transparent
                      icon
                      onPress={this.props.setAllShowSubcategoriesToFalse}&gt;
                      &lt;Eicon name="close-o" size={25} color="#FFFFFF" /&gt;
                    &lt;/Button&gt;
                  &lt;/Right&gt;
                &lt;/View&gt;
                &lt;Content style={catStyle.categoryStyle.modalContent}&gt;
                  &lt;SelectMultiple
                    checkboxSource={require('../../../images/unchecked.png')}
                    selectedCheckboxSource={require('../../../images/checked.png')}
                    labelStyle={[
                      styles.label,
                      styles.formElementHeight,
                      styles.modalListItem
                    ]}
                    items={controller.getDisplayedSubcategories(
                      this.props.categories
                    )}
                    selectedItems={controller.getSelectedSubcategories(
                      this.props.categories
                    )}
                    onSelectionsChange={(selections, item: Subcategory) =&gt; {
                      this.props.toggleSubcategory({ subcategory: item }).the
                    }}
                  /&gt;
                &lt;/Content&gt;
              &lt;/Container&gt;
            &lt;/Modal&gt;
          &lt;/View&gt;
          {this.props.error &amp;&amp; (
            &lt;Label
              disabled={false}
              style={[
                Typography.formLabel,
                { color: 'red' },
                { marginBottom: Spacing.medium }
              ]}&gt;
              {this.props.error}
            &lt;/Label&gt;
          )}
        &lt;/View&gt;
      )
    }
  }
  
  const catStyle = {
    // eslint-disable-next-line no-undef
    getBorderBottomWidth: (item: C.Category): number =&gt; {
      if (item.icon === 'shopping-basket') {
        return Spacing.none
      }
      return Spacing.none
    },
    // eslint-disable-next-line no-undef
    categoriesViewStyle: (props: C.CategoriesViewProps, categoriesToDisplay) =&gt; {
      return {
        backgroundColor: Colors.borderLeftColor(
          getIsCategoriesValid(props.categories)
        ),
        ...Corners.rounded,
        paddingLeft: Spacing.medium,
        height: Distances.FormElementHeights.Medium * categoriesToDisplay.length,
        overflow: 'hidden',
        borderBottomWidth: Spacing.none
      }
    },
    arrow: {
      color: Colors.brownDark,
      borderBottomColor: Colors.brownDark
    },
    icon: { height: Distances.FormElementHeights.Medium },
    // eslint-disable-next-line no-undef
    categoriesTitleStyle: {
      ...styles.title,
      ...Typography.titleLeftAlign
    },
    categoryStyle: {
      modalContent: {
        ...Corners.rounded
      }
    },
    modal: {
      flex: 0.7,
      height: 20,
      marginTop: Spacing.auto,
      marginBottom: Spacing.auto
    },
    modalView: {
      backgroundColor: Colors.white,
      height: 500,
      ...Corners.rounded
    },
    modalConatinerStyle: {
      marginBottom: Spacing.medium,
      color: Colors.brownDark,
      backgroundColor: Colors.brownLight,
      position: 'absolute',
      zIndex: ZIndexes.Layers.Negative,
      right: Spacing.none,
      height: Distances.Distances.Full,
      width: Distances.Distances.Full,
      ...Corners.rounded
    }
  }
  
  const categoriesListStyle = {
    flex: Distances.FlexDistances.Full,
    color: Colors.brownDark,
    backgroundColor: Colors.brownLight,
    height: Distances.FormElementHeights.Double,
    ...Corners.notRounded,
    marginRight: Spacing.medium
  }
  
  const border = (item: C.Category) =&gt; {
    return {
      borderBottomWidth: catStyle.getBorderBottomWidth(item),
      borderBottomColor: Colors.brownMedium
    }
  }
  
  const listItem = {
    height: Distances.FormElementHeights.Medium
  }
  
  // $FlowFixMe
  const CategoriesMultiselect = connect(
    mapStateToProps,
    mapDispatchToProps
  )(CategoriesMultiselectView)
  
  export default CategoriesMultiselect
  </code></pre>