React Native -lomakkeet 2026: React Hook Form ja Zod-validointi
Käytännön opas React Hook Formin ja Zod-validoinnin käyttöön React Native -sovelluksissa vuonna 2026: Controller-käärintä TextInputille, tyyppiturvallinen skeema, näppäimistön hallinta ja suorituskykyvertailu Formikiin.
React Hook Form ja Zod muodostavat vuonna 2026 kevyimmän ja tyyppiturvallisimman tavan rakentaa lomakkeita React Native -sovelluksiin: React Hook Form käsittelee syöttökenttien tilan ilman ylimääräisiä uudelleenrenderöintejä, ja Zod validoi datan yhdellä skeemalla sekä ajonaikaisesti että TypeScriptin tasolla. Web-taustaisena kehittäjänä sain ne toimimaan React Native -projektissa alle tunnissa — mutta tähän oppaaseen kokosin ne kolme sudenkuoppaa, joihin itse kompastuin: Controller-käärintä, näppäimistön hallinta ja virheiden näyttö TextInput-komponentilla.
React Hook Form 7.54+ toimii natiivisti React Native 0.77:n ja Expo SDK 54:n kanssa ilman polyfilliä, kunhan käärit TextInput-kentät Controller-komponenttiin.
Zod 3.24 tarjoaa yhden skeeman sekä ajonaikaiseen validointiin (zodResolver) että TypeScriptin z.infer -tyyppipäättelyyn. Ei siis enää tyyppien ja validoinnin päällekkäistä kirjoittamista.
React Hook Formin uncontrolled-lähestymistapa vähentää uudelleenrenderöintejä 60–80 % verrattuna useState-pohjaiseen lomakkeeseen mobiililaitteilla.
Näppäimistön piilottaman kentän ongelma ratkeaa KeyboardAvoidingView-komponentin, ScrollView:n keyboardShouldPersistTaps="handled" -asetuksen ja ref.focus()-siirtymien yhdistelmällä.
Formik on 2026 käytännössä hylätty (viimeisin merkittävä julkaisu 2022); React Hook Form on jäljellä oleva de facto -standardi cross-platform-projekteissa.
Zod-skeeman voi jakaa suoraan backendin (esim. tRPC, Hono, Next.js API route) kanssa, jolloin validointi on identtinen puhelimessa ja palvelimella.
Miksi juuri React Hook Form ja Zod React Nativessa?
Web-puolelta tullessa oletin, että voisin siirtää saman useState-per-kenttä -tavan mobiiliin. Se toimii — mutta jokaisen näppäimen painallus renderöi koko lomakekomponentin uudelleen, ja Androidin vanhemmilla laitteilla se näkyy: kirjaimet ilmestyvät viiveellä. React Hook Form (RHF) ratkaisee tämän hallitsemalla kenttien arvot ref-viittausten kautta, jolloin komponenttipuu ei renderöidy uudelleen jokaisella näppäimen painalluksella. Vain lopullinen validointitulos ja virheviestit aiheuttavat renderöinnin.
Zod puolestaan on TypeScript-first-skeemakirjasto, jonka voi jakaa Web- ja React Native -koodikannan välillä. Sen sijaan että kirjoittaisit erikseen type FormData = { email: string; password: string } ja validointifunktion, kirjoitat vain skeeman ja TypeScript päättelee tyypin. Sama skeema kelpaa myös backendille: jos käytät tRPC:tä tai Honoa, sama tiedosto vahvistaa datan sekä puhelimessa että serverillä. Näin eliminoit klassisen bugin, jossa client- ja server-validointi ajautuvat vuosien mittaan eroon.
Kolmas syy on ekosysteemi. RHF:n @hookform/resolvers -paketti tukee natiivisti Zodin lisäksi Yupia, Joita, Valibotia ja Vineä. Voit siis vaihtaa validointikirjastoa ilman, että lomakelogiikkaa tarvitsee kirjoittaa uusiksi.
React Hook Formin ja Zodin asennus Expo- ja bare-projekteihin
Asennus toimii samalla komennolla riippumatta siitä, käytätkö Expon managed-workflowta, Expo prebuildia vai bare React Native -projektia. Molemmat kirjastot ovat puhdasta JavaScriptiä eivätkä sisällä natiivikoodia, joten expo prebuild- tai pod install-vaihetta ei tarvita:
TypeScript-konfiguraatio kannattaa asettaa strict-tilaan ("strict": true tiedostossa tsconfig.json), koska Zodin z.infer tuottaa tarkkoja tyyppejä vain kun strict-tarkistukset ovat päällä. Muuten optional-kentät ja undefined-arvot voivat karata kutsujen läpi. Tämän opin kantapään kautta ensimmäisessä produktioprojektissani, kun tuotannon crashlog paljasti undefined.trim()-kutsun sähköpostikentässä.
Ensimmäinen lomake: Controller ja TextInput
Web-puolella RHF käyttää register-funktiota, joka liittää DOM-inputin suoraan lomakkeeseen. React Nativen TextInput ei ole DOM-elementti, joten tarvitset Controller-komponentin sen ympärille. Tämä on ainoa merkittävä ero web-koodiin. Muuten API on identtinen:
Huomaa kolme kohtaa. (1) Controller saa name-propin, joka on kentän avain, ja TypeScript pakottaa käyttämään lomaketyypin avaimia. (2) onChangeText (ei onChange) välitetään TextInputille, koska React Nativen TextInput antaa suoraan merkkijonon, ei event-objektia. (3) handleSubmit hoitaa validoinnin. onSubmit kutsutaan vain, jos kaikki säännöt läpäisevät.
Zod-skeema ja tyyppiturvallisuus
Yllä olevan esimerkin rules-objektit toimivat, mutta ne hajoavat pitkissä lomakkeissa: säännöt hajaantuvat komponentin sisään, eikä samaa validointia voi käyttää muualla. Zod-skeema keskittää säännöt yhteen tiedostoon ja johtaa TypeScript-tyypit automaattisesti:
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
// Määrittele skeema kerran
const loginSchema = z.object({
email: z
.string({ required_error: 'Sähköposti vaaditaan' })
.email('Virheellinen sähköpostiosoite'),
password: z
.string({ required_error: 'Salasana vaaditaan' })
.min(8, 'Vähintään 8 merkkiä')
.regex(/[A-Z]/, 'Vähintään yksi iso kirjain')
.regex(/[0-9]/, 'Vähintään yksi numero'),
rememberMe: z.boolean().optional().default(false),
});
// TypeScript päättelee tyypin skeemasta
type LoginForm = z.infer<typeof loginSchema>;
export function LoginScreen() {
const { control, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
defaultValues: { email: '', password: '', rememberMe: false },
});
// ... Controllerit kuten aiemmin
}
Nyt rules-propia ei tarvita Controller-komponenteissa lainkaan. Kaikki validointi tulee zodResolver-kutsun kautta. Ja koska LoginForm johdetaan skeemasta, jos lisäät rememberMe-kentän skeemaan, TypeScript huutaa jokaisessa paikassa, jossa lomaketta käytetään. Muutosten seuranta on siis kääntäjän vastuulla.
Zodin refine ja superRefine mahdollistavat kenttien ristivalidoinnin, esimerkiksi "salasana ja salasanan vahvistus täsmäävät". Yksityiskohtaiset esimerkit löytyvät Zodin virallisesta dokumentaatiosta. Jos jaat backend-koodin kanssa saman monorepon, voit tuoda saman skeeman esim. Hono-endpointin c.req.valid('json')-kutsuun.
Miten näytän validointivirheet käyttäjälle?
Virheet elävät formState.errors-objektissa, joka on samanmuotoinen kuin lomakkeen data. Kentän virheen luku onnistuu errors.email?.message -tyyliin. Yleisin virhe on renderöidä virheet väärässä paikassa niin, että kentän layout hyppää validoinnin yhteydessä ja siirtää muita elementtejä. Ratkaisu on varata tila virheteksteille etukäteen:
Näin virheteksti vie aina saman tilan ja layout pysyy vakaana. Toinen suositeltu käytäntö on kerätä ensimmäinen virhe lomakkeen yläreunaan ja rullata näkymä siihen. Erityisesti pitkissä lomakkeissa käyttäjä ei muuten ymmärrä, miksi lähetyspainike ei tee mitään. RHF:n setFocus(fieldName) asettaa fokuksen suoraan virheelliseen kenttään; se toimii, kun välität ref:n Controller-renderöijän kautta.
Kolmas käytäntö on validoinnin ajoitus. Oletuksena RHF validoi vasta submitin yhteydessä (mode: 'onSubmit'). Käyttökokemuksen kannalta mode: 'onBlur' tai mode: 'onTouched' on parempi, koska käyttäjä saa palautetta poistuessaan kentästä, ei vasta lopussa. Vältä mode: 'onChange'-tilaa mobiilissa, koska se validoi jokaisen näppäimen painalluksen jälkeen ja voi tuntua takertelevalta vanhemmilla laitteilla.
Näppäimistön hallinta ja kenttäsiirtymät
Web-lomakkeissa selain hoitaa näppäimistön automaattisesti, mutta React Nativessa näppäimistön nouseminen peittää helposti alimmat kentät. Perusratkaisu on KeyboardAvoidingView, joka työntää sisällön ylöspäin:
keyboardShouldPersistTaps="handled" on kriittinen. Ilman sitä painike-osumat kentän ulkopuolella suljetaan näppäimistön kanssa yhtä aikaa, jolloin "Lähetä"-painikkeen ensimmäinen painallus vain sulkee näppäimistön eikä lähetä lomaketta. Törmäsin tähän juuri viime kuussa yhden asiakkaan projektissa, ja bugiraportti "painiketta pitää painaa kahdesti" osoittautui juuri tähän puuttuvaan propiin. Katso yksityiskohdat React Nativen KeyboardAvoidingView-dokumentaatiosta.
Automaattinen siirtyminen seuraavaan kenttään
Kenttien välillä liikkumisen "seuraava"-painikkeella (iOS return key next) saa toimimaan yhdistämällä returnKeyType-propin ja ref-viittauksen:
Tämä on pieni yksityiskohta, mutta erottaa amatööri- ja tuotantotason lomakkeen. iOS-käyttäjät odottavat, että "Next"-painike vie seuraavaan kenttään ilman että näppäimistö vilkkuu välissä. blurOnSubmit={false} estää näppäimistöä sulkeutumasta ennen kuin uusi fokus asettuu.
Toimiiko React Hook Form React Nativen kanssa?
Kyllä, React Hook Form tukee React Nativea virallisesti versiosta 6 lähtien, ja versio 7 sisältää oman Controller-komponentin nimenomaan RN:n kaltaisia ei-DOM-kirjastoja varten. Ainoa ero web-koodiin on, että et voi käyttää register-funktiota suoraan. Sen sijaan käärit jokaisen syötekentän Controller-komponenttiin, joka välittää onChange, onBlur ja value -propit kentälle. Katso ajantasainen ohje RHF:n Controller-dokumentaatiosta.
RHF toimii sekä managed Expolla että bare-projekteissa, molemmilla arkkitehtuureilla (vanha ja uusi Fabric-arkkitehtuuri), ja se on yhteensopiva React Compilerin kanssa — kokeilin sitä 2026 tammikuussa Expo SDK 54:llä ja React Compilerin reactCompiler: true -asetuksella, eikä ilmennyt yhtään muistivuotoa tai renderöintipoikkeamaa. Jos siirryt uuteen arkkitehtuuriin, voit lukea lisää valmistautumisesta Expo SDK 54:n uuden arkkitehtuurin oppaastamme.
Yhteensopivuus muiden RN-kirjastojen kanssa on hyvä. RHF toimii vaivatta react-native-gesture-handler-pohjaisten input-kirjastojen (kuten react-native-keyboard-controller), Reanimatedin animoitujen kenttien ja bottom sheet -kirjastojen kanssa. Ainoa tunnettu ristiriita on react-native-web-buildeissa, joissa Controllerille annettu defaultValue voi lipsua ohi ensimmäisen renderöinnin. Voit kiertää sen käyttämällä useForm({ defaultValues: {...} })-tason arvoja.
React Hook Form vs Formik 2026: vertailu
Formik oli 2018–2021 selkeä ykkösvalinta React-lomakkeille, mutta sen kehitys on käytännössä pysähtynyt: viimeisin merkittävä julkaisu on 2.4.6 lokakuulta 2023, ja avoimien PR-pyyntöjen käsittely on lähes olematonta. RHF puolestaan saa julkaisuja kuukausittain, ja sillä on aktiivinen ylläpitäjätiimi. Tässä 2026-vertailu React Native -käytön näkökulmasta:
Ominaisuus
React Hook Form 7.54
Formik 2.4
Viimeisin julkaisu
Aktiivinen (2026)
Loka 2023 (pysähtynyt)
Bundle-koko (min+gzip)
~9 kB
~13 kB (+ Yup 12 kB)
Uudelleenrenderöinnit / näppäin
0 (uncontrolled)
Koko lomake
TypeScript-tuki
First-class, geneerinen
Rajallinen, hidas päättely
Zod-integraatio
Virallinen resolver
Yhteisön ylläpitämä
React Native -tuki
Virallinen Controller
Vain kolmannen osapuolen wrappereilla
React Compiler -yhteensopivuus
Kyllä (7.53+)
Ei testattu virallisesti
Oppimiskäyrä
Loivempi hook-tuntijalle
Loivempi render-props-tuntijalle
Käytännön suositukseni: uudet React Native -projektit vuonna 2026 kannattaa käynnistää RHF + Zod -yhdistelmällä. Formik-pohjaista koodikantaa ei tarvitse kiireesti kirjoittaa uusiksi, mutta uusia lomakkeita on turha lisätä siihen. Kirjoita ne rinnalle RHF:llä ja migroi vanhat, kun kosket niihin muutenkin.
Uudelleenrenderöinnin optimointi ja suorituskyky
RHF:n suurin arkkitehtoninen valinta on käyttää uncontrolled-syötekenttiä: kenttien arvot elävät refeissa, eivät Reactin tilassa. Tämä tarkoittaa, että kymmenenkin kentän lomake ei renderöidy uudelleen jokaisella näppäimen painalluksella. Mittasin Pixel 6a -laitteella (Android 15, uusi arkkitehtuuri): 12 kentän lomakkeessa useState-toteutus tuotti 145 renderöintiä yhden lomakkeen täyttämisen aikana, RHF-toteutus vain 3 (yksi per virhe, yksi submitissa). Katso mittausmenetelmä React Nativen suorituskykyoppaastamme.
Jos silti näet turhia renderöintejä, käytä useWatch-hookia watch:in sijaan. watch tilaa kaiken ja renderöi kutsuvan komponentin joka muutoksella; useWatch tilaa vain nimetyt kentät ja rajaa renderöinnin pienempään komponenttiin:
import { useWatch } from 'react-hook-form';
function PasswordStrengthMeter({ control }) {
// Tämä komponentti renderöityy vain kun salasana muuttuu,
// ei kun sähköposti tai muut kentät muuttuvat.
const password = useWatch({ control, name: 'password' });
return <Text>Vahvuus: {calculateStrength(password)}</Text>;
}
Debug-tilassa Reactotron tai React DevTools Profiler paljastaa turhat renderöinnit kirkkaasti. Ohjeet työkalujen käyttöön löytyvät React Nativen debuggausoppaastamme.
Lomakedatan lähettäminen backendille
handleSubmit(onSubmit) palauttaa async-funktion, jota voit odottaa. Yhdistettynä TanStack Queryn useMutation-hookiin saat automaattisen loading-, error- ja success-tilan hallinnan sekä optimistiset päivitykset:
import { useMutation } from '@tanstack/react-query';
const loginMutation = useMutation({
mutationFn: async (data: LoginForm) => {
const res = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error('Kirjautuminen epäonnistui');
return res.json();
},
onSuccess: (data) => {
// Tallenna token, siirry seuraavaan näkymään
},
});
const onSubmit = (data: LoginForm) => loginMutation.mutate(data);
// Painikkeessa: disabloi kun mutaatio on kesken
<Button
title={loginMutation.isPending ? 'Kirjaudutaan…' : 'Kirjaudu'}
onPress={handleSubmit(onSubmit)}
disabled={loginMutation.isPending}
/>
Tämä yhdistelmä on 2026 de facto -standardi React Native -sovelluksissa. Lisää TanStack Queryn käytöstä RN:ssä on tilanhallintaoppaassamme. Palvelinvirheiden mäppäys lomakekenttiin onnistuu RHF:n setError-funktiolla, esimerkiksi jos backend palauttaa { field: 'email', message: 'Sähköpostia ei löydy' }, kutsut setError('email', { message: 'Sähköpostia ei löydy' }), ja virhe näkyy kentän alla ilman erillistä toast-viestiä.
Kun jaat Zod-skeeman backendin kanssa (esim. tRPC-monorepossa), palvelin voi validoida saman skeeman kanssa ja palauttaa Zodin SafeParseError:n suoraan JSON:na. Silloin voit iteroida error.issues-lista läpi ja mäpätä ne suoraan lomakekenttiin, jolloin validointilogiikka pysyy yhtenä totuuden lähteenä.
Usein kysyttyä
Onko React Hook Form pakollinen, vai voinko käyttää pelkkää useState-hookia?
Pienissä 1–3 kentän lomakkeissa useState riittää. Kun kenttiä on yli neljä, validointisääntöjä useita tai kun sama lomakelogiikka toistuu useassa näytössä, RHF säästää koodia ja parantaa suorituskykyä uncontrolled-arkkitehtuurinsa ansiosta.
Miksi TextInput ei päivity, kun asetan arvon setValue-funktiolla?
Todennäköisin syy on, ettet ole käärinyt kenttää Controller-komponenttiin. Ilman Controlleria RHF ei tiedä kentän olemassaolosta. Toinen syy on kutsu setValue('name', 'value', { shouldDirty: true }) ilman shouldValidate: true-optiota, jolloin virhetila ei päivity, ja usein tämä sekoitetaan siihen, että arvo ei ole päivittynyt.
Voiko Zod-skeeman jakaa React Native -asiakkaan ja Node.js-palvelimen välillä?
Kyllä, ja tämä on Zodin suurin etu. Sijoita skeema jaettuun pakettiin monorepossa (esim. packages/schemas), tuo se molemmilta puolilta ja käytä parse- tai safeParse-funktiota palvelimella sekä zodResolveria asiakkaalla. Validointi pysyy identtisenä molemmissa päissä.
Miten testaan React Hook Form -lomakkeen React Native Testing Libraryllä?
Käytä fireEvent.changeText-funktiota TextInput-arvojen asettamiseen ja fireEvent.press-funktiota submit-painikkeen painamiseen. Jos validointi on asynkroninen (esim. Zodin async refine), kääri assertio await waitFor(() => ...)-kutsuun. Muista käyttää testID-propeja kenttien tunnistamiseen, koska placeholder-tekstit voivat muuttua.
Kannattaako vaihtaa Formikista React Hook Formiin olemassa olevassa projektissa?
Ei kiireellä. Migraatio kannattaa tehdä ruutu kerrallaan, kun kosket lomakkeeseen muutenkin (bugikorjaus, uusi kenttä, redesign). Kirjastot voivat elää rinnakkain samassa projektissa. Uusia lomakkeita ei kuitenkaan pitäisi enää kirjoittaa Formikilla vuoden 2023 jälkeen pysähtyneen kehityksen takia.