Form Engine
useForm()
useForm is a controlled form hook. It requires a Zod schema. Import z from the SDK — no separate zod install needed.
import { useForm, z } from '@objectifthunes/limestone-sdk';
const schema = z.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
function LoginForm() {
const form = useForm({
schema,
defaultValues: { email: '', password: '' },
onSubmit: async (values) => {
const api = useApi(); // or pass via closure
await api.post('/sessions', { body: values, skipAuth: true });
},
});
}
Config
| Field | Type | Required | Description |
|---|---|---|---|
schema | ZodObject<T> | Yes | Zod schema that defines the shape and validation rules |
defaultValues | Record<keyof T, string> | Yes | Initial field values |
onSubmit | (values: T) => Promise<void> | Yes | Called after full-form validation passes |
UseFormReturn fields
const form = useForm({ schema, defaultValues, onSubmit });
| Field | Type | Description |
|---|---|---|
values | Record<keyof T, string> | Current field values |
errors | Partial<Record<keyof T, string>> | Error message per field, or empty |
isDirty | boolean | True once any setValue has been called |
isSubmitting | boolean | True while onSubmit promise is pending |
setValue(field, value) | void | Update one field, marks isDirty = true |
validateField(field) | void | Parse only that field’s schema; updates its error |
handleSubmit() | Promise<void> | Validate all fields, then call onSubmit if valid |
reset() | void | Restore defaultValues, clear all errors and flags |
setServerErrors(errors) | void | Merge a Partial<Record<keyof T, string>> into errors |
Field-level validation
Call validateField in an onBlur handler to give immediate feedback as the user leaves a field.
<TextInput
value={form.values.email}
onChangeText={(v) => form.setValue('email', v)}
onBlur={() => form.validateField('email')}
/>
{form.errors.email ? <Text style={styles.error}>{form.errors.email}</Text> : null}
validateField parses only that field using the schema. It does not block or trigger submission.
Form-level validation
handleSubmit runs the full schema parse. If any field fails, errors are populated and onSubmit is not called.
<Pressable onPress={form.handleSubmit} disabled={form.isSubmitting}>
<Text>{form.isSubmitting ? 'Signing in…' : 'Sign in'}</Text>
</Pressable>
While isSubmitting is true, the button is disabled and the form ignores additional handleSubmit calls.
Server error merging
After a failed API call, use setServerErrors to display backend validation messages inline.
const form = useForm({
schema,
defaultValues: { email: '', password: '' },
onSubmit: async (values) => {
try {
await api.post('/sessions', { body: values, skipAuth: true });
router.push('/home');
} catch (e) {
// API returns { errors: { email: 'Already in use' } }
const body = (e as any).body;
if (body?.errors) {
form.setServerErrors(body.errors);
}
}
},
});
Server errors merge into form.errors alongside any client-side errors. A subsequent setValue on the same field clears that field’s error.
Reset after navigation
Call reset() when the form is dismissed or the user navigates away to avoid stale state.
React.useEffect(() => {
return () => { form.reset(); };
}, []);
Complete login form example
import React from 'react';
import { View } from 'react-native';
import { useForm, useApi, useTheme, z } from '@objectifthunes/limestone-sdk';
import { TextInput, Pressable, Text, Stack } from '@objectifthunes/limestone-sdk';
const loginSchema = z.object({
email: z.string().email('Enter a valid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export function LoginForm({ onSuccess }: { onSuccess: () => void }) {
const api = useApi();
const { tokens } = useTheme();
const form = useForm({
schema: loginSchema,
defaultValues: { email: '', password: '' },
onSubmit: async (values) => {
try {
await api.post('/sessions', { body: values, skipAuth: true });
onSuccess();
} catch (e) {
form.setServerErrors({ email: 'Invalid email or password' });
}
},
});
return (
<Stack gap="md">
<TextInput
label="Email"
value={form.values.email}
onChangeText={(v) => form.setValue('email', v)}
onBlur={() => form.validateField('email')}
error={form.errors.email}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
label="Password"
value={form.values.password}
onChangeText={(v) => form.setValue('password', v)}
onBlur={() => form.validateField('password')}
error={form.errors.password}
secureTextEntry
/>
<Pressable
onPress={form.handleSubmit}
disabled={form.isSubmitting}
bg="primary"
rounded="md"
p="md"
center
>
<Text color="primaryForeground" weight="semibold">
{form.isSubmitting ? 'Signing in…' : 'Sign in'}
</Text>
</Pressable>
</Stack>
);
}
TypeScript inference
The schema type flows through to values, errors, setValue, validateField, and setServerErrors. You get full autocomplete on field names.
// TypeScript error: 'username' is not a key of loginSchema
form.setValue('username', '');
// TypeScript error: argument type mismatch
form.setServerErrors({ unknownField: 'bad' });