Skip to main content

Getting Started

@codeleap/form is the CodeLeap package for reusable form state, field declarations, validation, and input binding.

Use it when the app needs to standardize:

  • multi-field forms declared outside component lifecycle
  • standalone reusable fields
  • validation attached to field definitions
  • package-aware input binding through register(...) or field
  • imperative prefill, submit, reset, and first-invalid flows
  • app-level field option extension through ExtraFieldOptions

Introduction

The form system provides a structured and scalable way to manage forms using validation, default values, and field configurations.

Installing the Library

Install @codeleap/form using Bun:

bun add @codeleap/form

Initial example

Import form from the library and use it:

src/form.ts
import { fields, form } from '@codeleap/form'
import { t } from '@lingui/core/macro'

export const example = form('example', {
input: fields.text({
placeholder: 'Placeholder',
}),
})

Choose the right form shape first

This is the main decision to make before writing code.

Use form(...) when

  • the screen has multiple related inputs
  • submit depends on several values together
  • you need values, isValid, validate(), firstInvalid(), or resetValues()
  • the UI should bind package-aware inputs through form.register(...)

Use a standalone field when

  • there is only one isolated control
  • you still want validation and shared field state
  • the component accepts field={...}
  • you want the field definition to be reused across several places

Use plain local state when

  • the input is truly local and temporary
  • the package would add little value
  • you do not need shared field behavior or validation helpers

register(...) vs useField(...)

These two patterns solve different problems.

  • use form.register('name') when the input component already understands CodeLeap form field props
  • use field={...} for standalone reusable field flows
  • use useField(...) inside a custom input component that needs to adapt a field into controlled UI state

Do not assume register(...) directly returns plain controlled props like value, onValueChange, error, or onBlur.

The important contract is that register(...) forwards the field prop surface expected by package-aware inputs.

Complete submit flow

This is the full pattern: declare → pre-fill on navigate → validate → submit → reset.

src/forms/editProfile.ts
import { form, fields, zodValidator } from '@codeleap/form'
import { z } from 'zod'

// 1. Declare OUTSIDE any component
export const editProfileForm = form('editProfile', {
name: fields.text({
label: 'Name',
validate: zodValidator(z.string().min(2, 'Name is too short')),
}),
email: fields.text({
label: 'Email',
validate: zodValidator(z.string().email('Invalid email')),
}),
})
src/screens/UserList.tsx
import { editProfileForm } from '../forms/editProfile'

// 2. Call setValues at the moment of navigation, before the screen mounts
function UserList() {
const handleEditPress = (user) => {
editProfileForm.setValues({
name: user.name,
email: user.email,
})

navigation.navigate('EditProfile', { userId: user.id })
}

// ...
}
src/screens/EditProfile.tsx
import { editProfileForm } from '../forms/editProfile'
import { usersManager } from '../query/users'

// 3. The screen just reads and submits — no pre-fill logic here
function EditProfileScreen({ userId }) {
const updateMutation = usersManager.useUpdate()

const handleSubmit = () => {
// 4. Validate all fields
if (!editProfileForm.isValid) return

// 5. Submit with current values
updateMutation.mutate({ id: userId, ...editProfileForm.values }, {
onSuccess: () => {
// 6. Reset and go back
editProfileForm.resetValues()
navigation.goBack()
},
})
}

return (
<>
<TextInput {...editProfileForm.register('name')} />
<TextInput {...editProfileForm.register('email')} />
<Button
onPress={handleSubmit}
disabled={updateMutation.isPending}
text={updateMutation.isPending ? 'Saving...' : 'Save'}
/>
</>
)
}

Prefill rule

Prefer imperative form.setValues(...) before opening or navigating to the edit UI.

Good pattern:

  1. user taps Edit
  2. app already has the item data
  3. app calls form.setValues(...)
  4. app opens the screen or modal

Do not default to reactive prefill logic inside the form screen when the values are already known at the moment the UI is opened.

Declare the Global Type

This is used to show the additional options that can be declared per field and passed to the input.

index.d.ts
import { TextInputProps } from 'react-native'

declare module '@codeleap/form' {
export interface ExtraFieldOptions extends TextInputFields {
placeholder?: string
label?: string
secure?: boolean
}
}

ExtraFieldOptions is a consumer-app extension point. It is how the app teaches fields about UI-specific options like labels, placeholders, descriptions, masking, disabled state, or keyboard hints.