Formik - 子组件意外重置整个表单状态
Formik - Child component unexpectedly resets entire form state
我有一个带有一些 textInputs 和一个自定义 CategoriesMultiselect
组件的表单,该组件弹出一个带有 SelectMultiple
的模式。当我 select 来自 multiselect 的一个选项时,它会调度一个 redux 动作,然后我的父表单被完全重置,除了我的类别。为什么要重置父表单?
具有以下形式的父组件:
//@flow
import * as Yup from 'yup'
import { withFormik } from 'formik'
import { Container } from 'native-base'
import * as React from 'react'
import { ScrollView, View } 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 { isLocationValid } from 'src/components/product/add/groceryItem/selector'
import { getSelectedSubcategories } from 'src/components/formControls/categoriesMultiselect/controller'
const mapStateToProps = (
state: RecordOf<VepoState>,
ownProps: { rerenderKey: boolean }
) => ({
locationListDisplayed: state.formControls.root.locationListDisplayed,
// $FlowFixMe
categories: getSelectedSubcategories(state.formControls.categories),
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<*>): Object => ({
updateAddProductImage: (value): void => {
dispatch(updateAddProductImage({ value }))
},
uploadAddProduct: (product: Product): void => {
dispatch(uploadAddProduct(product))
},
updateRerenderKey: () => {
dispatch(updateRerenderKey())
}
})
export const getLocationIsValid = (place: Place): boolean => {
return Object.keys(place).length > 0 ? true : false
}
type AddGroceryStoreState = {
name: string,
brand: string,
description: string,
price?: number
}
class AddGroceryItemView extends React.Component {
render() {
const {
values,
handleSubmit,
setFieldValue,
errors,
touched,
setFieldTouched,
isValid,
isSubmitting
} = this.props
console.log(errors)
console.log(this.props)
return (
<Container>
<VepoHeader title={'Add Vegan Grocery Product'} />
<Container style={container}>
<ScrollView
keyboardShouldPersistTaps="always"
style={viewStyle(this.props.locationListDisplayed).scrollView}>
<View>
<LocationAutocomplete
label={'Grocery Store'}
placeHolder={'Enter Grocery Store'}
/>
</View>
<View style={viewStyle().detailsContainer}>
<ImagePicker
label={'Product Image (optional)'}
image={this.props.image.image}
updateAddProductImage={this.props.updateAddProductImage}
updateRerenderKey={this.props.updateRerenderKey}
/>
<Input
label={'Product Name'}
onTouch={setFieldTouched}
value={values.name}
placeholder="Enter Name"
name="name"
required
error={touched.name && errors.name}
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
label={'Product Brand'}
value={values.brand}
onTouch={setFieldTouched}
error={touched.brand && errors.brand}
placeholder="Enter Brand"
name="brand"
required
onChange={setFieldValue}
deleteText={setFieldValue}
/>
<View>
<Input
label={'Product Description'}
value={values.description}
placeholder="Enter Description"
multiline={true}
required
onTouch={setFieldTouched}
error={touched.description && errors.description}
numberOfLines={4}
name="description"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
isValid={true}
isPrice={true}
label={'Product Price'}
value={values.price}
onTouch={setFieldTouched}
error={touched.price && errors.price}
placeholder="Enter Price"
name="price"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<View>
<CategoriesMultiselect.View
value={values.categories}
values={values}
error={errors.categories}
name="categories"
label="Product Categories"
categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
/>
</View>
</View>
</View>
</ScrollView>
</Container>
<Button.View
title="submit"
onPress={handleSubmit}
label={'GO!'}
disabled={!isValid || isSubmitting}
loading={isSubmitting}
/>
{/* <Button.View onSub={this._handleSubmit} onPress={this._handleSubmit} label={'GO!'} /> */}
</Container>
)
}
}
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().required(),
description: Yup.string()
.min(9)
.required(),
price: Yup.number()
.typeError('price must be a number')
.required()
}),
enableReinitialize: true,
mapPropsToValues: (props) => ({
name: '',
brand: '',
description: '',
price: '',
categories: props.categories
}),
handleSubmit: (values, { props }) => {
props.updateRerenderKey()
},
displayName: 'AddGroceryItemView'
})(AddGroceryItemView)
// $FlowFixMe
const AddGroceryItemViewComponent = connect(
mapStateToProps,
mapDispatchToProps
)(formikEnhancer)
export default AddGroceryItemViewComponent
重置表单的子组件:
//@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<VepoState>) => ({
vepo: state,
// $FlowFixMe
selectedCategory: selectSelectedCategory(state),
categories: state.formControls.categories
})
// eslint-disable-next-line flowtype/no-weak-types
const mapDispatchToProps = (dispatch: Dispatch<*>): Object => ({
setAllShowSubcategoriesToFalse: (): void => {
dispatch(setAllShowSubcategoriesToFalse())
},
toggleSubcategory: (sc): void => {
dispatch(toggleSubcategory(sc))
},
toggleShowSubcategories: (c): void => {
dispatch(toggleShowSubcategories(c))
},
updateAlertModalIsOpen: (isOpen: boolean): void => {
dispatch(updateAlertModalIsOpen(isOpen))
},
updateAlertModalMessage: (message: string): void => {
dispatch(updateAlertModalMessage(message))
},
updateAlertModalHasYesNo: (hasYesNo: boolean): void => {
dispatch(updateAlertModalHasYesNo(hasYesNo))
},
updateAlertModalTitle: (title: string): void => {
dispatch(updateAlertModalTitle(title))
}
})
const handleClick = (props, item) => {
controller.categoryClicked(props, item)
}
const renderCategoryRow = (props: C.CategoriesViewProps, item: C.Category) => {
return (
<View>
<ListItem style={listItem} icon onPress={() => handleClick(props, item)}>
<Left>
<Icon
style={styles.icon}
name={item.icon}
size={20}
color={item.iconColor}
/>
</Left>
<Body style={[styles.formElementHeight, border(item)]}>
<Text style={Typography.brownLabel}>{item.label}</Text>
</Body>
<Right style={[styles.formElementHeight, border(item)]}>
<Eicon style={catStyle.arrow} name="chevron-right" size={30} />
</Right>
</ListItem>
</View>
)
}
const getCategoriesToDisplay = (props) => {
const y = props.categories.filter((x) => props.categoryCodes.includes(x.code))
return y
}
let CategoriesMultiselect: React.ComponentType<C.CategoriesViewProps> = (
props: C.CategoriesViewProps
): Node => {
const categoriesToDisplay = getCategoriesToDisplay(props)
return (
<View>
<View style={{ ...Containers.fullWidthRow }}>
<Label disabled={false} style={Typography.formLabel}>
{props.label}
</Label>
<View style={{ ...Containers.fullWidthRow }} />
<Label disabled={false} style={Typography.formLabel}>
{controller.getNumberOfSelectedSubcategories(props.categories)}{' '}
Selected
</Label>
</View>
<View style={catStyle.categoriesViewStyle(props, categoriesToDisplay)}>
{props.categories && props.categories.length > 0 && (
<List
listBorderColor={'white'}
style={categoriesListStyle}
dataArray={categoriesToDisplay}
renderRow={(item: C.Category) => {
return renderCategoryRow(props, item)
}}
/>
)}
<View style={catStyle.modalConatinerStyle} />
<Modal
style={catStyle.modal}
isVisible={
props.categories
? props.categories.some((cat: C.Category) =>
controller.showModal(cat)
)
: false
}>
<Container style={catStyle.modalView}>
<View style={Modals.modalHeader}>
<Title style={catStyle.categoriesTitleStyle}>
{controller.getDisplayedCategoryLabel(props.categories)}
</Title>
<Right>
<Button
transparent
icon
onPress={props.setAllShowSubcategoriesToFalse}>
<Eicon name="close-o" size={25} color="#FFFFFF" />
</Button>
</Right>
</View>
<Content style={catStyle.categoryStyle.modalContent}>
<SelectMultiple
checkboxSource={require('../../../images/unchecked.png')}
selectedCheckboxSource={require('../../../images/checked.png')}
labelStyle={[
styles.label,
styles.formElementHeight,
styles.modalListItem
]}
items={controller.getDisplayedSubcategories(props.categories)}
selectedItems={controller.getSelectedSubcategories(
props.categories
)}
onSelectionsChange={(selections, item: Subcategory) =>
props.toggleSubcategory({ subcategory: item })
}
/>
</Content>
</Container>
</Modal>
</View>
{props.error && (
<Label
disabled={false}
style={[
Typography.formLabel,
{ color: 'red' },
{ marginBottom: Spacing.medium }
]}>
{props.error}
</Label>
)}
</View>
)
}
const catStyle = {
// eslint-disable-next-line no-undef
getBorderBottomWidth: (item: C.Category): number => {
if (item.icon === 'shopping-basket') {
return Spacing.none
}
return Spacing.none
},
// eslint-disable-next-line no-undef
categoriesViewStyle: (props: C.CategoriesViewProps, categoriesToDisplay) => {
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) => {
return {
borderBottomWidth: catStyle.getBorderBottomWidth(item),
borderBottomColor: Colors.brownMedium
}
}
const listItem = {
height: Distances.FormElementHeights.Medium
}
// $FlowFixMe
CategoriesMultiselect = connect(
mapStateToProps,
mapDispatchToProps
)(CategoriesMultiselect)
export default CategoriesMultiselect
这部分似乎重置了父表单:
onSelectionsChange={(selections, item: Subcategory) =>
props.toggleSubcategory({ subcategory: item })
}
这是因为 categories
发生了变化,因为它是 Redux 存储的一部分,它会触发 Formik 的 mapPropsToValues
,除了 categories
之外的所有内容的初始值为空字符串。解决方法是停止在 Formik 的 mapPropsToValues
类别中使用 redux 存储,它变成:
mapPropsToValues: () => ({
name: '',
brand: '',
description: '',
price: '',
categories: []
}),
然后更新 categories
的 Formik 表单值,将 Formik 的 setFieldValue
传递给子组件并使用它。
父级:
<CategoriesMultiselect.View
error={errors.categories}
setFieldValue={setFieldValue}
name="categories"
label="Product Categories"
categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
/>
在CategoriesMultiselect.View
中:
class CategoriesMultiselectView extends React.Component {
setFormCategories = () => {
if (this.props && this.props.setFieldValue) {
this.props.setFieldValue(
'categories',
controller.getSelectedSubcategories(this.props.categories)
)
}
}
render(): React.Node {
return (
<View>
<Modal
style={catStyle.modal}
onModalHide={this.setFormCategories}
isVisible={
...
</View>
我的回答是问题标题,而不是 OP 发布的具体问题。
我刚遇到类似的事情;更改 child 组件中的单个 Formik 值会意外地导致更改 Formik 的不相关部分。在我的例子中,特别是 touched
字段将重置为未被触摸,即使它们之前都被触摸过。
原因是 initialValues
被传递给 useFormik
的误用。 initialValues
在 parent 中动态生成,用于设置每个 child 的起始值。没问题。问题是,当 child 值被更改时,会启动一个回调,该回调会返回并更改 initialValues
以匹配 children 中的新 values
,这然后传回 useFormik
,新的起始值传回给 children。因为新的 initialValues
掩盖了一般的 values
本来的样子,所以很难看出有什么问题。但是由于新的 initialValues
进来,它导致 Formik 完全重置 touched
字段,因为(我相信,我实际上并没有深入挖掘 Formik 的源代码)它实际上是在制作一个全新的 Formik object,不是上一个的延续。
TL;DR: 如果您看到您的 Formik 中发生了奇怪的重置,请查看您传入的 initialValues
。如果他们曾经更改,这很可能是重置的原因。
我有一个带有一些 textInputs 和一个自定义 CategoriesMultiselect
组件的表单,该组件弹出一个带有 SelectMultiple
的模式。当我 select 来自 multiselect 的一个选项时,它会调度一个 redux 动作,然后我的父表单被完全重置,除了我的类别。为什么要重置父表单?
具有以下形式的父组件:
//@flow
import * as Yup from 'yup'
import { withFormik } from 'formik'
import { Container } from 'native-base'
import * as React from 'react'
import { ScrollView, View } 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 { isLocationValid } from 'src/components/product/add/groceryItem/selector'
import { getSelectedSubcategories } from 'src/components/formControls/categoriesMultiselect/controller'
const mapStateToProps = (
state: RecordOf<VepoState>,
ownProps: { rerenderKey: boolean }
) => ({
locationListDisplayed: state.formControls.root.locationListDisplayed,
// $FlowFixMe
categories: getSelectedSubcategories(state.formControls.categories),
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<*>): Object => ({
updateAddProductImage: (value): void => {
dispatch(updateAddProductImage({ value }))
},
uploadAddProduct: (product: Product): void => {
dispatch(uploadAddProduct(product))
},
updateRerenderKey: () => {
dispatch(updateRerenderKey())
}
})
export const getLocationIsValid = (place: Place): boolean => {
return Object.keys(place).length > 0 ? true : false
}
type AddGroceryStoreState = {
name: string,
brand: string,
description: string,
price?: number
}
class AddGroceryItemView extends React.Component {
render() {
const {
values,
handleSubmit,
setFieldValue,
errors,
touched,
setFieldTouched,
isValid,
isSubmitting
} = this.props
console.log(errors)
console.log(this.props)
return (
<Container>
<VepoHeader title={'Add Vegan Grocery Product'} />
<Container style={container}>
<ScrollView
keyboardShouldPersistTaps="always"
style={viewStyle(this.props.locationListDisplayed).scrollView}>
<View>
<LocationAutocomplete
label={'Grocery Store'}
placeHolder={'Enter Grocery Store'}
/>
</View>
<View style={viewStyle().detailsContainer}>
<ImagePicker
label={'Product Image (optional)'}
image={this.props.image.image}
updateAddProductImage={this.props.updateAddProductImage}
updateRerenderKey={this.props.updateRerenderKey}
/>
<Input
label={'Product Name'}
onTouch={setFieldTouched}
value={values.name}
placeholder="Enter Name"
name="name"
required
error={touched.name && errors.name}
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
label={'Product Brand'}
value={values.brand}
onTouch={setFieldTouched}
error={touched.brand && errors.brand}
placeholder="Enter Brand"
name="brand"
required
onChange={setFieldValue}
deleteText={setFieldValue}
/>
<View>
<Input
label={'Product Description'}
value={values.description}
placeholder="Enter Description"
multiline={true}
required
onTouch={setFieldTouched}
error={touched.description && errors.description}
numberOfLines={4}
name="description"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<Input
isValid={true}
isPrice={true}
label={'Product Price'}
value={values.price}
onTouch={setFieldTouched}
error={touched.price && errors.price}
placeholder="Enter Price"
name="price"
deleteText={setFieldValue}
onChange={setFieldValue}
/>
<View>
<CategoriesMultiselect.View
value={values.categories}
values={values}
error={errors.categories}
name="categories"
label="Product Categories"
categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
/>
</View>
</View>
</View>
</ScrollView>
</Container>
<Button.View
title="submit"
onPress={handleSubmit}
label={'GO!'}
disabled={!isValid || isSubmitting}
loading={isSubmitting}
/>
{/* <Button.View onSub={this._handleSubmit} onPress={this._handleSubmit} label={'GO!'} /> */}
</Container>
)
}
}
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().required(),
description: Yup.string()
.min(9)
.required(),
price: Yup.number()
.typeError('price must be a number')
.required()
}),
enableReinitialize: true,
mapPropsToValues: (props) => ({
name: '',
brand: '',
description: '',
price: '',
categories: props.categories
}),
handleSubmit: (values, { props }) => {
props.updateRerenderKey()
},
displayName: 'AddGroceryItemView'
})(AddGroceryItemView)
// $FlowFixMe
const AddGroceryItemViewComponent = connect(
mapStateToProps,
mapDispatchToProps
)(formikEnhancer)
export default AddGroceryItemViewComponent
重置表单的子组件:
//@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<VepoState>) => ({
vepo: state,
// $FlowFixMe
selectedCategory: selectSelectedCategory(state),
categories: state.formControls.categories
})
// eslint-disable-next-line flowtype/no-weak-types
const mapDispatchToProps = (dispatch: Dispatch<*>): Object => ({
setAllShowSubcategoriesToFalse: (): void => {
dispatch(setAllShowSubcategoriesToFalse())
},
toggleSubcategory: (sc): void => {
dispatch(toggleSubcategory(sc))
},
toggleShowSubcategories: (c): void => {
dispatch(toggleShowSubcategories(c))
},
updateAlertModalIsOpen: (isOpen: boolean): void => {
dispatch(updateAlertModalIsOpen(isOpen))
},
updateAlertModalMessage: (message: string): void => {
dispatch(updateAlertModalMessage(message))
},
updateAlertModalHasYesNo: (hasYesNo: boolean): void => {
dispatch(updateAlertModalHasYesNo(hasYesNo))
},
updateAlertModalTitle: (title: string): void => {
dispatch(updateAlertModalTitle(title))
}
})
const handleClick = (props, item) => {
controller.categoryClicked(props, item)
}
const renderCategoryRow = (props: C.CategoriesViewProps, item: C.Category) => {
return (
<View>
<ListItem style={listItem} icon onPress={() => handleClick(props, item)}>
<Left>
<Icon
style={styles.icon}
name={item.icon}
size={20}
color={item.iconColor}
/>
</Left>
<Body style={[styles.formElementHeight, border(item)]}>
<Text style={Typography.brownLabel}>{item.label}</Text>
</Body>
<Right style={[styles.formElementHeight, border(item)]}>
<Eicon style={catStyle.arrow} name="chevron-right" size={30} />
</Right>
</ListItem>
</View>
)
}
const getCategoriesToDisplay = (props) => {
const y = props.categories.filter((x) => props.categoryCodes.includes(x.code))
return y
}
let CategoriesMultiselect: React.ComponentType<C.CategoriesViewProps> = (
props: C.CategoriesViewProps
): Node => {
const categoriesToDisplay = getCategoriesToDisplay(props)
return (
<View>
<View style={{ ...Containers.fullWidthRow }}>
<Label disabled={false} style={Typography.formLabel}>
{props.label}
</Label>
<View style={{ ...Containers.fullWidthRow }} />
<Label disabled={false} style={Typography.formLabel}>
{controller.getNumberOfSelectedSubcategories(props.categories)}{' '}
Selected
</Label>
</View>
<View style={catStyle.categoriesViewStyle(props, categoriesToDisplay)}>
{props.categories && props.categories.length > 0 && (
<List
listBorderColor={'white'}
style={categoriesListStyle}
dataArray={categoriesToDisplay}
renderRow={(item: C.Category) => {
return renderCategoryRow(props, item)
}}
/>
)}
<View style={catStyle.modalConatinerStyle} />
<Modal
style={catStyle.modal}
isVisible={
props.categories
? props.categories.some((cat: C.Category) =>
controller.showModal(cat)
)
: false
}>
<Container style={catStyle.modalView}>
<View style={Modals.modalHeader}>
<Title style={catStyle.categoriesTitleStyle}>
{controller.getDisplayedCategoryLabel(props.categories)}
</Title>
<Right>
<Button
transparent
icon
onPress={props.setAllShowSubcategoriesToFalse}>
<Eicon name="close-o" size={25} color="#FFFFFF" />
</Button>
</Right>
</View>
<Content style={catStyle.categoryStyle.modalContent}>
<SelectMultiple
checkboxSource={require('../../../images/unchecked.png')}
selectedCheckboxSource={require('../../../images/checked.png')}
labelStyle={[
styles.label,
styles.formElementHeight,
styles.modalListItem
]}
items={controller.getDisplayedSubcategories(props.categories)}
selectedItems={controller.getSelectedSubcategories(
props.categories
)}
onSelectionsChange={(selections, item: Subcategory) =>
props.toggleSubcategory({ subcategory: item })
}
/>
</Content>
</Container>
</Modal>
</View>
{props.error && (
<Label
disabled={false}
style={[
Typography.formLabel,
{ color: 'red' },
{ marginBottom: Spacing.medium }
]}>
{props.error}
</Label>
)}
</View>
)
}
const catStyle = {
// eslint-disable-next-line no-undef
getBorderBottomWidth: (item: C.Category): number => {
if (item.icon === 'shopping-basket') {
return Spacing.none
}
return Spacing.none
},
// eslint-disable-next-line no-undef
categoriesViewStyle: (props: C.CategoriesViewProps, categoriesToDisplay) => {
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) => {
return {
borderBottomWidth: catStyle.getBorderBottomWidth(item),
borderBottomColor: Colors.brownMedium
}
}
const listItem = {
height: Distances.FormElementHeights.Medium
}
// $FlowFixMe
CategoriesMultiselect = connect(
mapStateToProps,
mapDispatchToProps
)(CategoriesMultiselect)
export default CategoriesMultiselect
这部分似乎重置了父表单:
onSelectionsChange={(selections, item: Subcategory) =>
props.toggleSubcategory({ subcategory: item })
}
这是因为 categories
发生了变化,因为它是 Redux 存储的一部分,它会触发 Formik 的 mapPropsToValues
,除了 categories
之外的所有内容的初始值为空字符串。解决方法是停止在 Formik 的 mapPropsToValues
类别中使用 redux 存储,它变成:
mapPropsToValues: () => ({
name: '',
brand: '',
description: '',
price: '',
categories: []
}),
然后更新 categories
的 Formik 表单值,将 Formik 的 setFieldValue
传递给子组件并使用它。
父级:
<CategoriesMultiselect.View
error={errors.categories}
setFieldValue={setFieldValue}
name="categories"
label="Product Categories"
categoryCodes={[CategoryEnums.CategoryCodes.Grocery]}
/>
在CategoriesMultiselect.View
中:
class CategoriesMultiselectView extends React.Component {
setFormCategories = () => {
if (this.props && this.props.setFieldValue) {
this.props.setFieldValue(
'categories',
controller.getSelectedSubcategories(this.props.categories)
)
}
}
render(): React.Node {
return (
<View>
<Modal
style={catStyle.modal}
onModalHide={this.setFormCategories}
isVisible={
...
</View>
我的回答是问题标题,而不是 OP 发布的具体问题。
我刚遇到类似的事情;更改 child 组件中的单个 Formik 值会意外地导致更改 Formik 的不相关部分。在我的例子中,特别是 touched
字段将重置为未被触摸,即使它们之前都被触摸过。
原因是 initialValues
被传递给 useFormik
的误用。 initialValues
在 parent 中动态生成,用于设置每个 child 的起始值。没问题。问题是,当 child 值被更改时,会启动一个回调,该回调会返回并更改 initialValues
以匹配 children 中的新 values
,这然后传回 useFormik
,新的起始值传回给 children。因为新的 initialValues
掩盖了一般的 values
本来的样子,所以很难看出有什么问题。但是由于新的 initialValues
进来,它导致 Formik 完全重置 touched
字段,因为(我相信,我实际上并没有深入挖掘 Formik 的源代码)它实际上是在制作一个全新的 Formik object,不是上一个的延续。
TL;DR: 如果您看到您的 Formik 中发生了奇怪的重置,请查看您传入的 initialValues
。如果他们曾经更改,这很可能是重置的原因。