Skip to content

Instantly share code, notes, and snippets.

@xantiagoma
Last active June 10, 2025 04:39
Show Gist options
  • Save xantiagoma/7716a67e380b5bc9414e3d24b5070273 to your computer and use it in GitHub Desktop.
Save xantiagoma/7716a67e380b5bc9414e3d24b5070273 to your computer and use it in GitHub Desktop.
import {
FormikState,
getIn,
FormikValues,
FieldConfig,
FieldInputProps,
FormikErrors,
FormikTouched,
FormikProps,
useFormik,
} from 'formik';
import { SetStateAction } from 'react';
/**
* createSubFormik
*
* Builds a "sub‐Formik" proxy that looks and behaves *exactly* like a `useFormik(...)` return value,
* but all the nested‐field calls (e.g. setFieldValue, getFieldProps, validateField, etc.)
* are internally rewritten to target a nested `basePath`.
*
* @template TParentValues The full parent form `values` type
* @template TSubValues The nested object type at `basePath`
*
* @param parentFormik The FormikProps<TParentValues> instance returned by useFormik<TParentValues>().
* @param basePath A Lodash‐style path string (e.g. "submissionMedias[2]" or "users['alice'].profile")
* pointing to a nested object in `parentFormik.values`.
* @returns A FormikProps<TSubValues>‐shaped object, with every property of the original
* FormikProps, but internally prefixing nested field calls by `basePath`.
*/
export function createSubFormik<
TParentValues extends FormikValues,
TSubValues extends FormikValues
>(
parentFormik: FormikProps<TParentValues>,
basePath: string
): ReturnType<typeof useFormik<TSubValues>> {
// ——————————————————————————————————————————————————————————————
// 1. Read nested "current" values/errors/touched via getIn()
// This safely digs into `parentFormik.values[basePath]`, even if intermediate objects are undefined.
// ——————————————————————————————————————————————————————————————
const subValues = getIn(parentFormik.values, basePath) as TSubValues;
const subErrors = (getIn(parentFormik.errors, basePath) ??
{}) as FormikErrors<TSubValues>;
const subTouched = (getIn(parentFormik.touched, basePath) ??
{}) as FormikTouched<TSubValues>;
// ——————————————————————————————————————————————————————————————
// 2. Likewise read the "initialX" variants from the parent's initial state
// so that subFormik.initialValues === initial parentFormik.values[basePath].
// ——————————————————————————————————————————————————————————————
const subInitialValues = getIn(
parentFormik.initialValues,
basePath
) as TSubValues;
const subInitialErrors = (getIn(parentFormik.initialErrors, basePath) ??
{}) as FormikErrors<TSubValues>;
const subInitialTouched = (getIn(parentFormik.initialTouched, basePath) ??
{}) as FormikTouched<TSubValues>;
const subInitialStatus = parentFormik.initialStatus
? (getIn(parentFormik.initialStatus, basePath) as any)
: undefined;
// ——————————————————————————————————————————————————————————————
// 3. Build the proxy object. Every property in FormikProps<TSubValues> must be present.
// When a method takes a `field: string` (e.g. "foo"), we prepend `${basePath}.` so that
// it truly targets `parentFormik.values[basePath].foo`.
// ——————————————————————————————————————————————————————————————
return {
// ——— Read‐Only "initial" props ——————————————————————————————————————
initialValues: subInitialValues,
initialErrors: subInitialErrors as FormikErrors<unknown>,
initialTouched: subInitialTouched as FormikTouched<unknown>,
initialStatus: subInitialStatus ?? undefined,
// ——— Core event‐handlers —————————————————————————————————————————————
/**
* handleBlur:
* If passed an event, we rewrite e.target.name = `${basePath}.${originalName}`.
* If passed a string, assume it's a "field name" (not typically used by children).
*/
handleBlur: ((eOrField: unknown) => {
if (typeof eOrField === 'string') {
// If child calls subFormik.handleBlur("someField"), this simply maps to parentFormik.handleBlur("submissionMedias[2].someField")
(parentFormik.handleBlur as (field: string) => void)(
`${basePath}.${eOrField}`
);
} else {
// If child calls subFormik.handleBlur(event), rewrite event.target.name accordingly
const e = eOrField as React.FocusEvent<unknown>;
(parentFormik.handleBlur as (e: React.FocusEvent<unknown>) => void)({
...e,
target: {
...e.target,
name: `${basePath}.${
(e.target as unknown as { name: string }).name
}`,
},
} as React.FocusEvent<unknown>);
}
}) as FormikProps<TSubValues>['handleBlur'],
/**
* handleChange:
* If passed an event, rewrite e.target.name to `${basePath}.${originalName}`.
* If passed a string (unlikely in most Formik usage), we can forward parentFormik.handleChange(name).
*/
handleChange: ((eOrField: unknown) => {
if (typeof eOrField === 'string') {
(parentFormik.handleChange as (field: string) => void)(
`${basePath}.${eOrField}`
);
} else {
const e = eOrField as React.ChangeEvent<unknown>;
const target = e.target as unknown as {
name: string;
type: string;
value: unknown;
checked: boolean;
};
const { name, type, value, checked } = target;
(parentFormik.handleChange as (e: React.ChangeEvent<unknown>) => void)({
...e,
target: {
...e.target,
name: `${basePath}.${name}`,
value: type === 'checkbox' ? checked : value,
},
} as React.ChangeEvent<unknown>);
}
}) as FormikProps<TSubValues>['handleChange'],
/**
* handleReset & handleSubmit: forwarded directly from parent. This means if the child calls
* `subFormik.handleSubmit(e)`, it will run the parent's onSubmit with the full values, not just sub-values.
* That is consistent with "one Formik context" for the entire form.
*/
handleReset: parentFormik.handleReset,
handleSubmit: parentFormik.handleSubmit,
/**
* resetForm(): forwarded directly; resets the entire parent form.
*/
resetForm: (nextState?: Partial<FormikState<TSubValues>>) => {
// For resetForm, we can't easily scope to just the sub-section, so we reset the entire form
// and optionally set the nested state if provided
if (nextState) {
// This is a complex operation - we'd need to merge the partial sub-state into the full parent state
// For now, we'll reset the entire form and then set the sub-values if provided
parentFormik.resetForm();
if (nextState.values) {
parentFormik.setFieldValue(basePath, nextState.values, false);
}
} else {
parentFormik.resetForm();
}
},
// ——— Imperative state‐setters —————————————————————————————————————
setErrors: (errs: FormikErrors<TSubValues>) => {
// Merge the child's errors under basePath back into the parent's full errors object
parentFormik.setErrors({
...parentFormik.errors,
[basePath]: errs,
} as unknown as FormikErrors<TParentValues>);
},
/**
* setFormikState:
* Allows child to do `subFormik.setFormikState(fnOrState)`. We forward it to parentFormik.setFormikState,
* which modifies the entire FormikState<TParentValues>. The child is responsible for only mutating
* its nested portion if desired. We do not rewrite the callback, because catching all possible user‐side
* mutations is extremely complex; instead, we forward "as is."
*/
setFormikState: (
stateOrCb:
| FormikState<TSubValues>
| ((state: FormikState<TSubValues>) => FormikState<TSubValues>)
) => {
// This is inherently unsafe but necessary for the proxy pattern
parentFormik.setFormikState(
stateOrCb as unknown as
| FormikState<TParentValues>
| ((state: FormikState<TParentValues>) => FormikState<TParentValues>)
);
},
/**
* setFieldTouched(field, touched?, shouldValidate?):
* We rewrite `${basePath}.${field}` under the hood.
*/
setFieldTouched: (
field: string,
touched: boolean = true,
shouldValidate?: boolean
): Promise<FormikErrors<TSubValues>> | Promise<void> =>
parentFormik.setFieldTouched(
`${basePath}.${field}`,
touched,
shouldValidate
) as Promise<FormikErrors<TSubValues>> | Promise<void>,
/**
* setFieldValue(field, value, shouldValidate?):
* We rewrite it to target `parentFormik.values[basePath].fieldName`.
*/
setFieldValue: (
field: string,
value: unknown,
shouldValidate?: boolean
): Promise<FormikErrors<TSubValues>> | Promise<void> =>
parentFormik.setFieldValue(
`${basePath}.${field}`,
value,
shouldValidate
) as Promise<FormikErrors<TSubValues>> | Promise<void>,
/**
* setFieldError(field, message):
* Rewrites to `parentFormik.setFieldError("submissionMedias[2].someField", message)`
*/
setFieldError: (field: string, message: string | undefined) =>
parentFormik.setFieldError(`${basePath}.${field}`, message),
/**
* setStatus(status):
* Status is a top-level concept. We forward directly to parentFormik.setStatus.
*/
setStatus: (status: unknown) => parentFormik.setStatus(status),
/**
* setSubmitting(isSubmitting):
* A top-level boolean; forwarded as‐is.
*/
setSubmitting: (isSubmitting: boolean) =>
parentFormik.setSubmitting(isSubmitting),
/**
* setTouched(touched, shouldValidate?):
* We merge the child's "touched" object under basePath into the parent's `touched`:
* parentFormik.touched = { … parentFormik.touched, [basePath]: touched }
*/
setTouched: (
touched: FormikTouched<TSubValues>,
shouldValidate?: boolean
): Promise<FormikErrors<TSubValues>> | Promise<void> => {
return parentFormik.setTouched(
{
...parentFormik.touched,
[basePath]: touched,
} as unknown as FormikTouched<TParentValues>,
shouldValidate
) as Promise<FormikErrors<TSubValues>> | Promise<void>;
},
/**
* setValues(values, shouldValidate?):
* If the child does subFormik.setValues(newSubValues), we must place those new subValues at
* parentFormik.values[basePath]. We assume "values" is either TSubValues or a callback
* (prevSub => TSubValues). We read current parent values, replace the slice at basePath,
* then call parentFormik.setValues(newParentValues).
*/
setValues: (
vals: SetStateAction<TSubValues>,
shouldValidate?: boolean
): Promise<FormikErrors<TSubValues>> | Promise<void> => {
// Extract current subValues from parentFormik.values
const currentParent = parentFormik.values;
const currentSub = getIn(currentParent, basePath) as TSubValues;
// Compute nextSub = typeof vals === 'function' ? vals(currentSub) : vals
const nextSub =
typeof vals === 'function'
? (vals as (prev: TSubValues) => TSubValues)(currentSub)
: vals;
// Now rebuild a brand‐new parent values object with updated nested path
// We can't just do `{ ...currentParent, [basePath]: nextSub }` because basePath may be deep.
// Instead, rely on Formik's setFieldValue for the entire object under basePath:
return parentFormik.setFieldValue(basePath, nextSub, shouldValidate) as
| Promise<FormikErrors<TSubValues>>
| Promise<void>;
},
// ——— Submission & Validation —————————————————————————————————————————
/**
* submitForm(): forwarded directly; runs the parent's submission logic.
*/
submitForm: parentFormik.submitForm,
/**
* validateForm(values?):
* If the child passes new sub-values to validate, we would need to reconstruct a full parent
* values object to run validation. For simplicity, if `values` is omitted, we forward directly.
* If `values` is provided (as TSubValues), you'd have to do the same "re‐embedding" we do in setValues.
* In most usage (child calls subFormik.validateForm() with no args), forwarding is fine.
*/
validateForm: (vals?: TSubValues): Promise<FormikErrors<TSubValues>> => {
if (typeof vals === 'undefined') {
return parentFormik.validateForm() as Promise<FormikErrors<TSubValues>>;
}
// Build a new "parentValuesCandidate" by injecting `vals` at basePath into parentFormik.values
parentFormik.setFieldValue(basePath, vals, false);
return parentFormik.validateForm() as Promise<FormikErrors<TSubValues>>;
},
/**
* validateField(name):
* Internally calls parentFormik.validateField(`${basePath}.${name}`)
*/
validateField: (name: string) =>
parentFormik.validateField(`${basePath}.${name}`),
// ——— Field Registration —————————————————————————————————————————————
/**
* registerField(name, opts):
* Prefix to `${basePath}.${name}`, so the parentFormik registers nested field metadata properly.
*/
registerField:
parentFormik.registerField as FormikProps<TSubValues>['registerField'],
/**
* unregisterField(name):
* Prefix likewise.
*/
unregisterField: (name: string) => {
parentFormik.unregisterField(`${basePath}.${name}`);
},
// ——— Field Helpers & Meta ————————————————————————————————————————
/**
* getFieldProps(nameOrOptions):
* If `nameOrOptions` is a string, rewrite `name = \`${basePath}.${nameOrOptions}\``.
* If it's a FieldConfig object, clone it but replace `.name` with `\`${basePath}.${.name}\``.
*/
getFieldProps: (<Value = unknown>(
nameOrOptions: string | FieldConfig<Value>
): FieldInputProps<Value> => {
if (typeof nameOrOptions === 'string') {
return parentFormik.getFieldProps(`${basePath}.${nameOrOptions}`);
}
// If it's an object like { name: 'foo', type: 'text', ... }, rewrite the `name` field
const opts = nameOrOptions as FieldConfig<Value>;
return parentFormik.getFieldProps({
...opts,
name: `${basePath}.${opts.name}`,
});
}) as FormikProps<TSubValues>['getFieldProps'],
/**
* getFieldMeta(name):
* Simply prefixes to read nested error/meta state.
*/
getFieldMeta: (name: string) =>
parentFormik.getFieldMeta(`${basePath}.${name}`),
/**
* getFieldHelpers(name):
* Prefix to call parentFormik.getFieldHelpers(`${basePath}.${name}`).
*/
getFieldHelpers: (name: string) =>
parentFormik.getFieldHelpers(`${basePath}.${name}`),
// ——— Booleans & Read‐Only Flags ——————————————————————————————————————
isValid: parentFormik.isValid,
dirty: parentFormik.dirty,
isSubmitting: parentFormik.isSubmitting,
isValidating: parentFormik.isValidating,
submitCount: parentFormik.submitCount,
// ——— Validation Flags —————————————————————————————————————————————
validateOnBlur: parentFormik.validateOnBlur ?? true,
validateOnChange: parentFormik.validateOnChange ?? true,
validateOnMount: parentFormik.validateOnMount ?? false,
// ——— Live Values/Errors/Touched/Status —————————————————————————————————
values: subValues,
errors: subErrors,
touched: subTouched,
status: parentFormik.status,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment