ViraStack
  • Academy
  • About
  • Support
HomeGet StartedExamples

Getting started

  • Introduction
  • Upgrade from 1.0
  • llms.txt
  • llms-full.txt

Usage

  • useViraPassword()
  • Strength
  • Confirm password
  • React Hook Form
  • Accessibility
  • Customization

Server

  • Server validation
  • Leaked passwords

Reference

  • Helpers
  • Types
  • UI libraries
  • Comparison

React Hook Form

Use the hook with register, Controller, Zod and ViraStack Mask in one form.

React Hook Form stays the single source of truth. The hook only adds props to the input; it never holds the form value.

register()

const { inputProps, btnProps, rule } = useViraPassword({ policy, value: password })
 
// inputProps holds attributes only: spread it before or after register()
<input {...inputProps} {...form.register("password", rule)} />
<button {...btnProps} />

useWatch()

reset() and setValue() change the input without an input event. Pass value so strength and matches follow them. useWatch also works with the React Compiler, unlike watch().

const password = useWatch({ control: form.control, name: "password" })
const { inputProps, strength } = useViraPassword({ policy, value: password })

rule & deps

rule returns the first error key. deps re-validates the confirm field when the password changes.

form.register("password", { ...rule, deps: ["confirm"] })
 
// deps only runs on user input. After setValue, check the confirm field yourself:
form.setValue("password", next)
form.trigger("confirm")

Give the form method="post". Before hydration a form submits natively, and the default GET puts the password in the URL.

Controller

<Controller
  control={form.control}
  name="password"
  render={({ field }) => <PasswordField field={field} />}
/>
 
function PasswordField({ field }: { field: ControllerRenderProps<Values, "password"> }) {
  const { inputProps } = useViraPassword({
    policy,
    value: field.value,
    inputProps: { ...field, onChange: (e) => field.onChange(e.target.value) },
  })
  return <input {...inputProps} />
}

Zod resolver

With a resolver, register rules don't run. Put the same policy into the Zod schema with evaluate:

// components/signup-form.tsx
"use client";
 
import { useState } from "react";
 
import { zodResolver } from "@hookform/resolvers/zod";
import { useViraPassword } from "@virastack/password";
import { evaluate, type PasswordIssue } from "@virastack/password/strength";
import { useForm, useWatch } from "react-hook-form";
import { z } from "zod";
 
import { passwordMessages } from "@/lib/password-messages";
import { policy } from "@/lib/password-policy";
 
// The same policy inside a Zod schema: errors are keys, messages come from your map.
const schema = z
  .object({ password: z.string(), confirm: z.string() })
  .superRefine(({ password, confirm }, ctx) => {
    for (const issue of evaluate(password, policy).issues) {
      ctx.addIssue({ code: "custom", path: ["password"], message: issue });
    }
    if (confirm !== password) {
      ctx.addIssue({ code: "custom", path: ["confirm"], message: "password.mismatch" });
    }
  });
 
export function SignupForm() {
  const [submitted, setSubmitted] = useState(false);
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { password: "", confirm: "" },
  });
  const { errors } = form.formState;
 
  // value from useWatch: reset() and setValue() don't fire input events.
  const password = useWatch({ control: form.control, name: "password" });
  const { inputProps, btnProps } = useViraPassword({ policy, value: password });
  const { inputProps: confirmProps, btnProps: confirmButton } = useViraPassword({
    matchWith: password,
  });
 
  return (
    <form method="post" onSubmit={form.handleSubmit(() => setSubmitted(true))}>
      <label htmlFor={inputProps.id}>New password</label>
      <div>
        <input {...inputProps} {...form.register("password")} />
        <button {...btnProps} />
      </div>
      {errors.password && (
        <p role="alert">{passwordMessages[errors.password.message as PasswordIssue]}</p>
      )}
 
      <label htmlFor={confirmProps.id}>Confirm password</label>
      <div>
        <input {...confirmProps} {...form.register("confirm")} />
        <button {...confirmButton} />
      </div>
      {errors.confirm && (
        <p role="alert">{passwordMessages[errors.confirm.message as PasswordIssue]}</p>
      )}
 
      <div>
        <button type="submit">Create account</button>
        <button type="button" onClick={() => (form.reset(), setSubmitted(false))}>
          Reset
        </button>
      </div>
      {submitted && <p>Valid. The password was hidden on submit.</p>}
    </form>
  );
}

With Mask

ViraStack Mask v2 also keeps the value in React Hook Form, so both share one useForm. Use them on different inputs, never on the same one.

// components/signup-form.tsx
"use client";
 
import { useState } from "react";
 
import { useViraMask } from "@virastack/mask";
import { useViraPassword } from "@virastack/password";
import { useForm, useWatch } from "react-hook-form";
 
import { passwordMessages } from "@/lib/password-messages";
import { policy } from "@/lib/password-policy";
 
type Values = { phone: string; password: string; confirm: string };
 
export function SignupForm() {
  const [values, setValues] = useState<Values | null>(null);
  // One form: React Hook Form holds every value. Mask and Password go on different inputs.
  const form = useForm<Values>({ defaultValues: { phone: "", password: "", confirm: "" } });
  const { phone } = useViraMask(form, { phone: { preset: "phone", country: "TR" } });
  const { errors } = form.formState;
 
  const password = useWatch({ control: form.control, name: "password" });
  const { inputProps, btnProps, rule, issues } = useViraPassword({ policy, value: password });
  const {
    inputProps: confirmProps,
    btnProps: confirmButton,
    rule: confirmRule,
  } = useViraPassword({ matchWith: password });
 
  return (
    <form method="post" onSubmit={form.handleSubmit(setValues)}>
      <label htmlFor="phone">Phone</label>
      <input id="phone" {...phone} placeholder="(555) 123 45 67" />
 
      <label htmlFor={inputProps.id}>New password</label>
      <div>
        {/* deps: re-check the confirm field when the password changes */}
        <input {...inputProps} {...form.register("password", { ...rule, deps: ["confirm"] })} />
        <button {...btnProps} />
      </div>
      {errors.password && <p role="alert">{passwordMessages[issues[0] ?? "password.tooShort"]}</p>}
 
      <label htmlFor={confirmProps.id}>Confirm password</label>
      <div>
        <input {...confirmProps} {...form.register("confirm", confirmRule)} />
        <button {...confirmButton} />
      </div>
      {errors.confirm && <p role="alert">{passwordMessages["password.mismatch"]}</p>}
 
      <button type="submit">Sign up</button>
      {values && (
        <pre>
          {JSON.stringify({ phone: values.phone, password: "•".repeat(values.password.length) })}
        </pre>
      )}
    </form>
  );
}
PreviousConfirm passwordNextAccessibility

On this page

  • register()
  • useWatch()
  • rule & deps
  • Controller
  • Zod resolver
  • With Mask
Something wrong? Fix it