In 2021, using 16.13.1, this way worked for me to satisfy several requirements:
- The submit/reset buttons cannot be nested within the
<Formik>
element. Note, if you can do this then you should use the useFormikContext
answer because it is simpler than mine. (Mine will allow you to change which form is being submitted (I have one app bar but multiple forms the user can navigate to).
- The external submit/reset buttons must be able to submit and reset the Formik form.
- The external submit/reset buttons must appear disabled until the form is dirty (the external component must be able to observe the Formik form's
dirty
state.)
Here's what I came up with: I created a new context provider dedicated to holding some helpful Formik stuff to link my two external components which are in different nested branches of the app (A global app bar and a form somewhere else, deeper in a page view – in fact, I need the submit/reset buttons to adapt to different forms the user has navigated to, not just one; not just one <Formik>
element, but only one at a time).
The following examples use TypeScript but if you only know javascript just ignore the stuff after colons and it's the same in JS.
You place <FormContextProvider>
high enough in your app that it wraps both of the disparate components that need to have access to Formik stuff. Simplified example:
<FormContextProvider>
<MyAppBar />
<MyPageWithAForm />
</FormContextProvider>
Here's 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>
}
In the component that renders the <Formik>
element, I add this line:
const { formikFormRef } = useContext(FormContext)
In the same component, I add this attribute to the <Formik>
element:
innerRef={formikFormRef}
In the same component, the first thing nested under the <Formik>
element is this (importantly, note the addition of the <FormContextRefreshConduit />
line).
<Formik
innerRef={formikFormRef}
initialValues={initialValues}
...
>
{({ submitForm, isSubmitting, initialValues, values, setErrors, errors, resetForm, dirty }) => (
<Form>
<FormContextRefreshConduit />
...
In my component that contains the submit/reset buttons, I have the following. Note the use of 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>
</>
)
}
The ref
is useful for calling Formik methods but is not normally able to be observed for its dirty
property (react won't trigger a re-render for this change). FormContextRefreshConduit
together with forceUpdate
are a viable workaround.
Thank you, I took inspiration from the other answers to find a way to meet all of my own requirements.