React Formik:在<Formik />之外使用submitForm

83

当前行为

<Formik
    isInitialValid
    initialValues={{ first_name: 'Test', email: 'test@mail.com' }}
    validate={validate}
    ref={node => (this.form = node)}
    onSubmitCallback={this.onSubmitCallback}
    render={formProps => {
        const fieldProps = { formProps, margin: 'normal', fullWidth: true, };
        const {values} = formProps;
        return (
            <Fragment>
                <form noValidate>
                    <TextField
                        {...fieldProps}
                        required
                        autoFocus
                        value={values.first_name}
                        type="text"
                        name="first_name"

                    />

                    <TextField
                        {...fieldProps}
                        name="last_name"
                        type="text"
                    />

                    <TextField
                        {...fieldProps}
                        required
                        name="email"
                        type="email"
                        value={values.email}

                    />
                </form>
                <Button onClick={this.onClick}>Login</Button>
            </Fragment>
        );
    }}
/>

我正在尝试这个解决方案https://github.com/jaredpalmer/formik/issues/73#issuecomment-317169770,但它总是返回Uncaught TypeError: _this.props.onSubmit is not a function

当我尝试console.log(this.form)时,会出现submitForm函数。

有什么解决方法吗?


- Formik 版本:最新 - React 版本:v16 - 操作系统:Mac OS

12个回答

112

如果有人想知道通过React hooks的解决方案是什么:

Formik 2.x,如此答案所述。

// import this in the related component
import { useFormikContext } from 'formik';

// Then inside the component body
const { submitForm } = useFormikContext();

const handleSubmit = () => {
  submitForm();
}

请注意,此解决方案仅适用于 Formik 组件内部的组件,因为它使用上下文 API。如果出于某种原因您想要从外部组件或实际使用 Formik 的组件手动提交,则仍然可以使用 innerRef 属性。

TLDR; 如果您要提交的组件是 <Formik>withFormik() 组件的子级,请使用此上下文解决方案;否则,请使用下面的 innerRef 解决方案。

Formik 1.5.x +

// Attach this to your <Formik>
const formRef = useRef()

const handleSubmit = () => {
  if (formRef.current) {
    formRef.current.handleSubmit()
  }
}

// Render
<Formik innerRef={formRef} />

1
这就是为什么Formik组件有一个innerRef属性,用于传递您想要附加的引用,如果我没记错的话:https://github.com/jaredpalmer/formik/blob/d5e1de97201da969e86d1dec589b306ed0e7794a/packages/formik/src/Formik.tsx#L1002 - Eric Martin
18
在 TypeScript 中使用 useRef 时,不要忘记设置 FormikValues 类型为 useRef<FormikValues>() - Andrii Verbytskyi
3
最佳的Formik 1.5.x解决方案。对于Formik 2.x,请参阅ZEE发布的解决方案。 - Dmitriusan
8
дҪҝз”ЁuseRefе’ҢTypeScriptпјҢжҲ‘еҸ‘зҺ°2021е№ҙзҡ„жӯЈзЎ®ж–№ејҸжҳҜuseRef<FormikProps<FormikValues>>(null)гҖӮ - ABCD.ca
1
似乎 innerRef 在 Formik 1.5.x 中无法正常工作。我一直在得到类似于示例中引用的 {current: null} - Vadym P
显示剩余3条评论

41
你可以将 formikProps.submitForm (Formik 的编程式提交)绑定到父组件上,然后从父组件触发提交:
import React from 'react';
import { Formik } from 'formik';

class MyForm extends React.Component {
    render() {
        const { bindSubmitForm } = this.props;
        return (
            <Formik
                initialValues={{ a: '' }}
                onSubmit={(values, { setSubmitting }) => {
                    console.log({ values });
                    setSubmitting(false);
                }}
            >
                {(formikProps) => {
                    const { values, handleChange, handleBlur, handleSubmit } = formikProps;

                    // bind the submission handler remotely
                    bindSubmitForm(formikProps.submitForm);

                    return (
                        <form noValidate onSubmit={handleSubmit}>
                            <input type="text" name="a" value={values.a} onChange={handleChange} onBlur={handleBlur} />
                        </form>
                    )
                }}
            </Formik>
        )
    }
}

class MyApp extends React.Component {

    // will hold access to formikProps.submitForm, to trigger form submission outside of the form
    submitMyForm = null;

    handleSubmitMyForm = (e) => {
        if (this.submitMyForm) {
            this.submitMyForm(e);
        }
    };
    bindSubmitForm = (submitForm) => {
        this.submitMyForm = submitForm;
    };
    render() {
        return (
            <div>
                <button onClick={this.handleSubmitMyForm}>Submit from outside</button>
                <MyForm bindSubmitForm={this.bindSubmitForm} />
            </div>
        )
    }
}

export default MyApp;

4
如何使用函数来实现?不太确定如何运用书籍进行研究。 - iwaduarte
1
@iwaduarte 你是指“钩子”吧 :-)?对我来说,这似乎是使用useRef的一个很好的案例。 - gotofritz
为“hook”版本添加了一个答案 :) - Eric Martin
看起来相当糟糕。如果这不是一个内部方法就好了。 - Sebastian Patten
1
这种方法太过于hacky了。我建议你使用formik中的ref,并在需要的地方使用它。 - Mario Petrovic
显示剩余3条评论

25

我遇到了相同的问题,找到了一个非常简单的解决方案,希望这可以帮到你:

这个问题可以通过纯 html 解决。如果在你的 form 上放置一个 id 标签,那么你可以使用按钮的 form 标签来针对它进行操作。

例如:

      <button type="submit" form="form1">
        Save
      </button>
      <form onSubmit={handleSubmit} id="form1">
           ....
      </form>
你可以把表单和按钮放在任何地方,甚至分开放置。 这个按钮将触发表单提交功能,formik会捕获并继续通常的流程。(只要表单在屏幕上呈现,而按钮也在呈现,那么无论表单和按钮放在哪里,都可以正常工作)

有什么缺点吗?看起来似乎是最好的解决方案。 - Sebastian Thomas
非常漂亮和干净的解决方案! - undefined

23

我找到的最佳解决方案在这里描述:https://dev59.com/YFQK5IYBdhLWcg3wDLkw#53573760

将"id"属性添加到您的表单中:id='my-form'

class CustomForm extends Component {
    render() {
        return (
             <form id='my-form' onSubmit={alert('Form submitted!')}>
                // Form Inputs go here    
             </form>
        );
    }
}

然后将相同的 Id 添加到表单之外目标按钮的 "form" 属性中:

<button form='my-form' type="submit">Outside Button</button>

现在,“外部按钮”按钮将与表单内的按钮完全等效。

注意:IE11不支持此功能。


5
2021年,使用16.13.1,以下方法对我满足了几个要求:
- 提交/重置按钮不能嵌套在<Formik>元素中。请注意,如果可以这样做,则应该使用useFormikContext答案,因为它比我的简单。(我的方法将允许您更改正在提交的表单(我有一个应用栏,但用户可以导航到多个表单)。) - 外部提交/重置按钮必须能够提交和重置Formik表单。 - 外部提交/重置按钮必须在表单被修改后出现禁用状态(外部组件必须能够观察Formik表单的dirty状态)。
我想出了以下方法:我创建了一个新的上下文提供者,专门用于保留一些有用的Formik信息,以链接我的两个外部组件,它们位于应用程序的不同嵌套分支中(全局应用栏和页面视图中的其他某个地方的表单——事实上,我需要提交/重置按钮适应用户导航到的不同表单,而不仅仅是一个;不仅仅是一个<Formik>元素,但每次只能有一个)。
以下示例使用TypeScript,但如果只了解JavaScript,请忽略冒号后面的内容,JS中相同。将<FormContextProvider>放置在应用程序中足够高的位置,以包装需要访问Formik信息的两个不同组件。简化示例:
<FormContextProvider>
  <MyAppBar />
  <MyPageWithAForm />
</FormContextProvider>

这是FormContextProvider:

import React, { MutableRefObject, useRef, useState } from 'react'
import { FormikProps, FormikValues } from 'formik'

export interface ContextProps {
  formikFormRef: MutableRefObject<FormikProps<FormikValues>>
  forceUpdate: () => void
}

/**
 * Used to connect up buttons in the AppBar to a Formik form elsewhere in the app
 */
export const FormContext = React.createContext<Partial<ContextProps>>({})

// https://github.com/deeppatel234/react-context-devtool
FormContext.displayName = 'FormContext'

interface ProviderProps {}

export const FormContextProvider: React.FC<ProviderProps> = ({ children }) => {
  // Note, can't add specific TS form values to useRef here because the form will change from page to page.
  const formikFormRef = useRef<FormikProps<FormikValues>>(null)
  const [refresher, setRefresher] = useState<number>(0)

  const store: ContextProps = {
    formikFormRef,
    // workaround to allow components to observe the ref changes like formikFormRef.current.dirty
    forceUpdate: () => setRefresher(refresher + 1),
  }

  return <FormContext.Provider value={store}>{children}</FormContext.Provider>
}

在呈现<Formik>元素的组件中,我增加了这行代码:
const { formikFormRef } = useContext(FormContext)

在同一个组件中,我在<Formik>元素上添加了这个属性:
innerRef={formikFormRef}

在同一个组件中,嵌套在<Formik>元素下的第一件事情是这个(重要的是,请注意添加了<FormContextRefreshConduit />行)。
<Formik
  innerRef={formikFormRef}
  initialValues={initialValues}
  ...
>
  {({ submitForm, isSubmitting, initialValues, values, setErrors, errors, resetForm, dirty }) => (
    <Form>
      <FormContextRefreshConduit />
      ...

在我包含提交/重置按钮的组件中,我有以下内容。请注意使用 formikFormRef
export const MyAppBar: React.FC<Props> = ({}) => {
  const { formikFormRef } = useContext(FormContext)
  
  const dirty = formikFormRef.current?.dirty

  return (
    <>
      <AppButton
        onClick={formikFormRef.current?.resetForm}
        disabled={!dirty}
      >
        Revert
      </AppButton>
      <AppButton
        onClick={formikFormRef.current?.submitForm}
        disabled={!dirty}
      >
        Save
      </AppButton>
    </>
  )
}

ref常用于调用Formik的方法,但通常无法观察其dirty属性(React不会触发重新渲染以反映此更改)。FormContextRefreshConduitforceUpdate结合使用是一种可行的解决方法。

谢谢,我受到其他答案的启发找到了一种满足自己所有要求的方法。


forceUpdate 添加的好。这就是 innerRef 的问题所在 - 它不会触发父组件中的重新渲染。你区分使用它来公开 Formik 方法(例如 submitFormresetForm)与使用它来观察状态(例如 dirtyisSubmitting)非常准确。非常有帮助。 - Brock Klein
@ABCD.ca,当您的Formik渲染时,它会调用其渲染函数,该函数会呈现<FormContextRefreshConduit/>,该组件使用FormContext调用forceUpdate来更改FormContext的状态。如何避免出现“警告:无法在呈现不同组件(Formik)的同时更新组件(FormContextProvider)”的情况? - THX-1138
@THX-1138 抱歉,我不知道,但它没有给我那个错误 - 似乎在不同的范围内会有所帮助。 - ABCD.ca
@ABCD.ca,有没有办法获取您的方法的可运行链接/代码?我正在遵循类似的方法,但在控制台(运行时)中收到了我引用的错误消息。 - THX-1138

5
如果您将类组件转换为函数式组件,自定义钩子useFormikContext提供了在树的任何位置使用提交的方法:
   const { values, submitForm } = useFormikContext();

注意:这仅适用于那些不需要在Formik组件外部调用submit的情况,所以您可以将您的Formik组件放置在组件树中的更高级别处,并使用自定义钩子useFormikContext,但如果确实需要从Formik之外提交,您将不得不使用一个useRef

<Formik innerRef={formikRef} />

https://formik.org/docs/api/useFormikContext


1
另外值得注意的是,useFormikContext 仅存在于 Formik 2.x。 - Dmitriusan
1
下树后,ref允许您在Formik组件外部使用,是吗? - ArmenB
我认为首选的方法是使用withFormik高阶组件来包装将执行提交操作的组件--> https://formik.org/docs/api/withFormik,以便能够调用useFormikContext。 - Didier Caron
提交按钮如果在组件外面会发生什么? - rony
11
这个问题是一个只有六个单词的句子。其中一个单词是“outside”。你如何把它作为正确答案? - Thomas Chafiol
显示剩余2条评论

2
如果你正在使用withFormik,这对我很有用:

  const handleSubmitThroughRef = () => {
    newFormRef.current.dispatchEvent(
      new Event("submit", { cancelable: true, bubbles: true })
    );
  };

Just put a regular react ref on your form:

  <form
        ref={newFormRef}
        
        onSubmit={handleSubmit}
      >

0
我已经在React类组件中逐步实现了这个功能:
1 - 我声明了一个“ref”变量来保持表单对象的引用,(useRef仅在函数组件中有效,因此我使用React.createRef()函数编写了以下代码)
constructor(props) {
  super(props);
  this.visitFormRef = React.createRef();
}

2 - 在formik表单上有一个"innerRef"功能,所以我已经将上面的ref变量分配给它:
<Formik 
    initialValues={initialValues}
    onSubmit={(values) => onSubmit(values)}
    validationSchema={validationSchema}
    enableReinitialize={true}
    innerRef={this.visitFormRef}  //<<--here
>

3- 为了触发表单的提交事件,我在表单之外声明了一个函数:

triggerFormSubmit = () => {
    
    if (this.visitFormRef.current) 
        this.visitFormRef.current.handleSubmit();
}

4- 最后,我从外部按钮调用了上面的函数:

<Button onClick={() => this.triggerFormSubmit()} />

注意不要混淆:onSubmit(values)函数分配给formik表单,仍然存在,并且获取表单值。我们只是在此处从外部按钮触发了它。

0

我是通过使用ref来模拟内部隐藏的提交按钮被点击,从而实现用户点击外部提交按钮的方式来实现我的目标。

const hiddenInnerSubmitFormRef = useRef(null);

const handleExternalButtonClick = () => {
  hiddenInnerSubmitFormRef.current.click();
}

return (
<>
  {/* External Button */}
  <button onClick={handleExternalButtonClick}>
   External Submit
  </button>

  <Formik onSubmit={values => console.log(values)}>
   {() => (
     <Form>

      {/* Hide Button /*}
      <button type="submit" ref={hiddenInnerSubmitFormRef} className="hidden">
        Submit
      </button>
     </Form>
   )}
  </Formik>
 </>
)

对于函数组件,这个答案会触发错误:Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? 因此最好使用此页面中的其他解决方案:https://dev59.com/g1UL5IYBdhLWcg3w-cLr#58223758 - Dika
@Dika 这就是我使用的确切方法,它没有触发任何错误。 - Idris
有两种可能性:要么您不使用功能组件,要么您使用的React版本比我的旧。 - Dika

0
另一种简单的方法是使用useState并将prop传递给子formik组件。在那里,您可以使用useEffect hook设置setState。
const ParentComponent = () => {
  const [submitFunc, setSubmitFunc] = useState()
  return <ChildComponent setSubmitFunc={setSubmitFunc}>
}

const ChildComponent= ({ handleSubmit, setSubmitFunc }) => {
   useEffect(() => {
     if (handleSubmit) setSubmitFunc(() => handleSubmit)
   }, [handleSubmit, setSubmitFunc])

   return <></>
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接