{"id":29362977,"url":"https://github.com/patternhelloworld/headless-formik-helper","last_synced_at":"2026-04-17T04:33:36.644Z","repository":{"id":270791320,"uuid":"911471742","full_name":"patternhelloworld/headless-formik-helper","owner":"patternhelloworld","description":"A Clean and Useful Helper Hook for useFormik","archived":false,"fork":false,"pushed_at":"2025-01-03T06:24:20.000Z","size":124,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-31T00:54:09.960Z","etag":null,"topics":["formik","formik-yup","headless","react"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/headless-formik-helper","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patternhelloworld.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-03T05:14:58.000Z","updated_at":"2025-01-03T06:23:58.000Z","dependencies_parsed_at":"2025-01-03T05:58:34.844Z","dependency_job_id":"dc07ebed-ea8e-4be6-909e-810ad765ed86","html_url":"https://github.com/patternhelloworld/headless-formik-helper","commit_stats":null,"previous_names":["patternhelloworld/headless-formik-helper"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/patternhelloworld/headless-formik-helper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patternhelloworld%2Fheadless-formik-helper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patternhelloworld%2Fheadless-formik-helper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patternhelloworld%2Fheadless-formik-helper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patternhelloworld%2Fheadless-formik-helper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patternhelloworld","download_url":"https://codeload.github.com/patternhelloworld/headless-formik-helper/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patternhelloworld%2Fheadless-formik-helper/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31915278,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"online","status_checked_at":"2026-04-17T02:00:06.879Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["formik","formik-yup","headless","react"],"created_at":"2025-07-09T09:22:01.522Z","updated_at":"2026-04-17T04:33:36.605Z","avatar_url":"https://github.com/patternhelloworld.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Headless-Formik-Helper\n\n\u003e A Clean and Useful Helper Hook for useFormik\n\n## Table of Contents\n- [Features](#features)\n- [Usage](#Usage)\n- [Sample Codes](#sample-codes)\n- [APIs](#apis)\n\n## Features\n\n- Show headless Formik examples using useFormik combined with Yup\n- Eager validation for all inputs\n- Functions for updating key-value pairs\n- Normalization of Formik values before sending them to the server\n- ...More features will be added over time\n\n## Usage\n- Registered in NPM\n```shell\nnpm install headless-formik-helper\n```\n\n## Sample Codes\n\n- Check comments in the sample code below.\n  - ```javascript\n      \"use client\";\n    \n      import React, {useContext, useEffect, useState} from \"react\";\n      import {FormikErrors, FormikHelpers, FormikProps, useFormik} from \"formik\";\n      import styles from \"./productManagement.module.scss\";\n      import {Button, Text} from \"@mantine/core\";\n\n      import {usePathname, useRouter, useSearchParams} from \"next/navigation\";\n    \n      import {\n          ProductType,\n          CreateProductInput,\n          UpdateProductInput,\n          useCreateProductMutation,\n          useFindProductLazyQuery,\n          useRemoveProductMutation,\n          useUpdateProductMutation,\n      } from \"@/generated/graphql\";\n    \n      import MemoizedTextInput from \"@/components/common/form-input/MemoizedTextInput\";\n      import MemoizedSelect from \"@/components/common/form-input/MemoizedSelect\";\n    \n      import {GlobalToastContext} from \"@/providers/GlobalToastProvider\";\n      import {UI_COMMON_VALUES} from \"@/util/value-util\";\n      import {\n          ExtendedCreateProductInput,\n          ExtendedUpdateProductInput,\n          PRODUCT_CREATE_INITIAL_VALUES,\n          PRODUCT_UPDATE_INITIAL_VALUES,\n          PRODUCT_VALIDATION_SCHEMA,\n      } from \"@/components/product/schema/crud-schema\";\n    \n      import {useRecoilState} from \"recoil\";\n      import {globalLoadingState} from \"@/recoil/common\";\n      import {useIsFirstRender} from \"@/hooks/useIsFirstRender\";\n      \n      // 0. Import : headless-formik-helper\n      import {useHeadlessFormikHelper} from \"headless-formik-helper\";\n      import {CreateOrUpdateMode} from \"headless-formik-helper/dist/types\";\n      \n      const ProductManagement = ({ PK_NAME }: { PK_NAME: string }) =\u003e {\n          const searchParams = useSearchParams();\n          const idForUpdate: number | null = Number(searchParams.get(PK_NAME));\n        \n          const isFirstRender = useIsFirstRender();\n          const router = useRouter();\n      \n          const { sendErrorMsgToGlobalToast, sendSuccessMsgToGlobalToast } = useContext(GlobalToastContext);\n          const [globalLoading, setGlobalLoading] = useRecoilState(globalLoadingState);\n        \n          const [\n              fetchProduct,\n              {\n              data: fetchProductData,\n              loading: fetchProductLoading,\n              error: fetchProductError,\n              },\n          ] = useFindProductLazyQuery();\n    \n          // 1. useFormik    \n          const formik: FormikProps\u003c\n              ExtendedCreateProductInput | ExtendedUpdateProductInput\n            \u003e   = useFormik\u003cExtendedCreateProductInput | ExtendedUpdateProductInput\u003e({\n              initialValues: {\n              ...(idForUpdate\n              ? PRODUCT_UPDATE_INITIAL_VALUES\n              : PRODUCT_CREATE_INITIAL_VALUES),\n              ...fetchProductData?.product\n              },\n              validationSchema: PRODUCT_VALIDATION_SCHEMA,\n              validateOnMount: false,\n              validateOnChange: true,\n              validateOnBlur: true,\n              enableReinitialize: true\n          });\n    \n          // 2. **useHeadlessFormikHelper**\n          const {\n              formikValuesChanged,\n              onKeyValueChangeByEventMemoized,\n              onKeyValueChangeByNameValueMemoized,\n              normalizeFormikValues,\n              } = useHeadlessFormikHelper({\n                  formik: formik,\n                  eagerValidationInitialOptions: {\n                      CREATE_OR_UPDATE: idForUpdate ? CreateOrUpdateMode.UPDATE : CreateOrUpdateMode.CREATE,\n                      afterMileSeconds: 0,\n                      keyNameToCheckFetchedForUpdate : \"id\"\n                  }\n          });\n    \n\n      const isSubmitDisabled = formik === undefined ? false : !(formik.isValid \u0026\u0026 formik.dirty);\n    \n      const [\n          createProduct,\n          {\n          data: createProductData,\n          loading: createProductLoading,\n          error: createProductError,\n      },\n      ] = useCreateProductMutation();\n      const [\n          updateProduct,\n          {\n          data: updateProductData,\n          loading: updateProductLoading,\n          error: updateProductError,\n      },\n      ] = useUpdateProductMutation();\n      const [\n          removeProduct,\n          {\n          data: removeProductData,\n          loading: removeProductLoading,\n          error: removeProductError,\n      },\n      ] = useRemoveProductMutation();\n    \n      const createOrUpdateProduct = () =\u003e {\n          if (formik.values.productType === UI_COMMON_VALUES.SELECT_OPTION_EMPTY) {\n              sendErrorMsgToGlobalToast(\"Product type is required.\", true);\n              return;\n          }\n        \n          // 3. normalizeFormikValues\n          if (!idForUpdate) {\n            createProduct({\n              variables: {\n                createProductInput: normalizeFormikValues\u003cCreateProductInput\u003e(formik.values),\n              },\n              onCompleted(data) {\n                sendSuccessMsgToGlobalToast(\"Product created successfully.\");\n                redirectToList();\n              },\n            });\n          } else {\n            updateProduct({\n              variables: {\n                pickProductInput: { id: idForUpdate },\n                updateProductInput: normalizeFormikValues\u003cUpdateProductInput\u003e(formik.values),\n              },\n              onCompleted(data) {\n                sendSuccessMsgToGlobalToast(\"Product updated successfully.\");\n                router.refresh();\n              },\n            });\n          }\n      };\n    \n      const fetchProductWrapper = () =\u003e {\n          if (idForUpdate) {\n              fetchProduct({\n                  variables: {\n                  pickProductInput: {\n                  id: idForUpdate,\n                  },\n              },\n           });\n          }\n      };\n    \n      const redirectToList = () =\u003e {\n          router.push(\"/products\");\n      };\n    \n      useEffect(() =\u003e {\n          fetchProductWrapper();\n      }, [idForUpdate]);\n    \n      useEffect(() =\u003e {\n          if (idForUpdate \u0026\u0026 fetchProductData \u0026\u0026 !fetchProductLoading) {\n              const product = fetchProductData.product;\n              formik.setValues(product);\n          }\n      }, [fetchProductData]);\n    \n      return (\n          \u003cdiv className={styles.mainContainer}\u003e\n              \u003cdiv className={styles.titleContainer}\u003e\n              \u003cspan className={styles.title}\u003e\n                Product {!idForUpdate ? \"Creation\" : \"Update\"}\n              \u003c/span\u003e\n          \u003c/div\u003e\n    \n            \u003cform className={styles.form}\u003e\n              \u003cdiv className={styles.field}\u003e\n                \u003cspan\u003e\n                  Product Name \u003cspan className=\"required-marker\"\u003e*\u003c/span\u003e\n                \u003c/span\u003e\n                \u003cMemoizedTextInput\n                  name=\"name\"\n                  value={formik.values.name || \"\"}\n                  error={formik.errors.name}\n                  touched={formik.touched.name}\n                  placeholder=\"Enter product name\"\n                  onChange={onKeyValueChangeByEventMemoized}\n                  onBlur={formik.handleBlur}\n                /\u003e\n              \u003c/div\u003e\n              \u003cdiv className={styles.field}\u003e\n                \u003cspan\u003eProduct Type\u003c/span\u003e\n                \u003cMemoizedSelect\n                  placeholder=\"Select product type\"\n                  data={[\"Physical\", \"Digital\"]}\n                  value={formik.values.productType || \"\"}\n                  onChange={(value) =\u003e {\n                    onKeyValueChangeByNameValueMemoized({\n                      name: \"productType\",\n                      value,\n                    });\n                  }}\n                  error={formik.errors.productType}\n                  touched={formik.touched.productType}\n                /\u003e\n              \u003c/div\u003e\n            \u003c/form\u003e\n            {formik.values.productType === ProductType.Physical \u0026\u0026 (\n              \u003cProductTypePhysical formik={formik} /\u003e\n            )}\n            {formik.values.productType === ProductType.Digital \u0026\u0026 (\n              \u003cProductTypeDigital formik={formik} /\u003e\n            )}\n            \u003cdiv className=\"z-10 flex items-center sticky bottom-0 h-14 w-full bg-white justify-end space-x-2 p-2\"\u003e\n              \u003cButton\n                className=\"w-28\"\n                sx={{\n                  color: \"black\",\n                  backgroundColor: \"#F6F6F6\",\n                  \"\u0026:hover\": {\n                    backgroundColor: \"#E0E0E0\",\n                  },\n                }}\n                onClick={() =\u003e redirectToList()}\n            \u003e\n                Cancel\n              \u003c/Button\u003e\n    \n              \u003cButton\n                className=\"w-28\"\n                sx={{\n                  backgroundColor: \"#26C8B9\",\n                  \"\u0026:hover\": {\n                    backgroundColor: \"#1BAA9A\",\n                  },\n                }}\n                onClick={() =\u003e {\n                  createOrUpdateProduct();\n                }}\n                disabled={isSubmitDisabled}\n            \u003e\n                {!idForUpdate ? \"Create\" : \"Update\"}\n              \u003c/Button\u003e\n            \u003c/div\u003e\n          \u003c/div\u003e\n      );\n      };\n    \n      export default React.memo(ProductManagement);\n\n    ```\n  - ```javascript\n      import * as Yup from \"yup\";\n      import {\n      ProductType,\n      CreateProductInput,\n      ProductOutput,\n      UpdateProductInput,\n      } from \"@/generated/graphql\";\n      import { PRODUCT_CATEGORY_TYPE } from \"@/components/product/meta/schema\";\n      import { UI_COMMON_VALUES } from \"@/util/value-util\";\n      import { YUP_EMPTY_VALUE_ERROR_MESSAGE } from \"@/util/yup-utils\";\n\n    /*\n    *   The purpose of defining these extended types is to accommodate UI-specific requirements that differ from server constraints.\n    *   For instance, \"productType\" is restricted to \"Physical\" and \"Digital\" on the server, but the UI may need to allow an empty value for better user experience.\n    */\n\n      export type ExtendedCreateProductInput = Omit\u003c\n        CreateProductInput,\n        \"productType\"\u003e   \u0026 {\n      productType: ProductType | \"-\";\n      };\n\n      export type ExtendedUpdateProductInput = Omit\u003c\n          UpdateProductInput,\n          \"productType\"\n        \u003e   \u0026 {\n          productType: ProductType | \"-\";\n      };\n\n      export const PRODUCT_VALIDATION_SCHEMA = Yup.object().shape({\n       name: Yup.string().required(YUP_EMPTY_VALUE_ERROR_MESSAGE),\n       sku: Yup.string()\n      .required(YUP_EMPTY_VALUE_ERROR_MESSAGE)\n      .matches(/^[a-zA-Z0-9_-]+$/, \"SKU must consist of alphanumeric characters, dashes, or underscores.\"),\n        category: Yup.mixed\u003cstring\u003e().oneOf(\n      Object.values(PRODUCT_CATEGORY_TYPE).map((value) =\u003e value.value?.toString()),\n      \"Invalid product category.\"\n      )\n      .required(\"Product category is required.\"),\n      price: Yup.number()\n      .required(YUP_EMPTY_VALUE_ERROR_MESSAGE)\n      .min(0, \"Price must be a positive number.\"),\n      stock: Yup.number()\n      .required(YUP_EMPTY_VALUE_ERROR_MESSAGE)\n      .min(0, \"Stock must be a non-negative number.\"),\n      productType: Yup.mixed\u003cProductType\u003e()\n      .oneOf(Object.values(ProductType) as ProductType[], \"Invalid product type.\")\n      .required(YUP_EMPTY_VALUE_ERROR_MESSAGE),\n      description: Yup.string().nullable(),\n      createdAt: Yup.date().nullable(),\n      updatedAt: Yup.date().nullable(),\n      });\n\n      export const PRODUCT_CREATE_INITIAL_VALUES = {\n          name: \"\",\n          sku: \"\",\n          category: UI_COMMON_VALUES.SELECT_OPTION_EMPTY, // Example: \"Electronics\"\n          price: 0, // Example: 100\n          stock: 0, // Example: 50\n          productType: UI_COMMON_VALUES.SELECT_OPTION_EMPTY, // \"Physical\" or \"Digital\"\n          description: \"\", // Example: \"This is a sample product description.\"\n          createdAt: undefined,\n          updatedAt: undefined,\n      };\n\n      export const PRODUCT_UPDATE_INITIAL_VALUES = {\n          name: \"\",\n          sku: \"\",\n          category: UI_COMMON_VALUES.SELECT_OPTION_EMPTY,\n          price: 0,\n          stock: 0,\n          productType: UI_COMMON_VALUES.SELECT_OPTION_EMPTY,\n          description: \"\",\n          createdAt: undefined,\n          updatedAt: undefined,\n      };\n    ```\n\n## APIs\n\nThe `headless-formik-helper` library provides the following utilities to enhance the functionality of Formik forms:\n\n### 1. **onKeyValueChangeByNameValue**\nUpdates a specific field in Formik using its name and a new value.\n\n**Signature:**\n```typescript\n({\n  name,\n  value,\n}: {\n  name: keyof T \u0026 string;\n  value: any;\n}) =\u003e void;\n```\n\n### 2. **onKeyValueChangeByNameIndexFieldValueMemoized**\nHandles updates for complex structures like arrays or objects with nested fields. Supports actions like adding or removing items.\n\n**Signature:**\n```typescript\n({\n  name,\n  index,\n  field,\n  value,\n  action,\n  newItem,\n}: {\n  name: keyof T \u0026 string;\n  index?: number;\n  field?: string;\n  value?: any;\n  action?: 'add' | 'remove';\n  newItem?: any;\n}) =\u003e void;\n```\n\n### 3. **normalizeFormikValues**\nNormalizes Formik values before sending them to the server, ensuring compliance with backend requirements.\n\n**Signature:**\n```typescript\n\u003cT extends Record\u003cstring, any\u003e\u003e(obj: T) =\u003e T;\n```\n\n### 4. **onKeyValueChangeByNameIndexFieldTouchedMemoized**\nMarks a specific field in a nested structure as \"touched\" based on its name, index, and field name.\n\n**Signature:**\n```typescript\n({\n  name,\n  index,\n  field,\n}: {\n  name: keyof T \u0026 string;\n  index: number;\n  field: string;\n}) =\u003e void;\n```\n\n### 5. **onKeyValueChangeByEvent**\nHandles changes in Formik fields triggered by standard React change events (e.g., `\u003cinput\u003e`).\n\n**Signature:**\n```typescript\n(e: React.ChangeEvent\u003cHTMLInputElement\u003e) =\u003e void;\n```\n\n### 6. **onKeyValueChangeByNameValueMemoized**\nA memoized version of `onKeyValueChangeByNameValue` for optimizing updates to Formik fields.\n\n**Signature:**\n```typescript\n({\n  name,\n  value,\n}: KeyValueChangeByNameValueMemoized\u003cT\u003e) =\u003e void;\n```\n\n### 7. **formikValuesChanged**\nIndicates whether Formik values have changed from their initial state.\n\n**Signature:**\n```typescript\nT | boolean;\n```\n\n### 8. **onKeyValueChangeByEventMemoized**\nA memoized version of `onKeyValueChangeByEvent` for efficiently handling React change events.\n\n**Signature:**\n```typescript\n(e: React.ChangeEvent\u003cHTMLInputElement\u003e) =\u003e void;\n```\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatternhelloworld%2Fheadless-formik-helper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatternhelloworld%2Fheadless-formik-helper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatternhelloworld%2Fheadless-formik-helper/lists"}