| | |

React Hook Form 18 ๐Ÿ“‹ Multi-Step Forms โ€” Preserving state across wizard steps

โฑ๏ธ Estimated read: 13 min | |

The multi-step form is not a different form API. It is the single useForm instance whose fields are spread across the multiple screens, and the state preservation is the mechanism that keeps the instance alive while the screens mount and unmount. The React Hook Form documentation states the rule plainly: “Fields from earlier steps unmount when the step changes, but their values stay in the form because shouldUnregister defaults to false. Don’t set it to true here” . The default is the mechanism that preserves the values, and the shouldUnregister: true is the trap that silently deletes the earlier steps the moment the user advances.

The single-instance pattern is the other half. A wizard that uses the separate useForm per step creates the separate form instances, and the separate instances do not share the values. The FormProvider and the useFormContext are the mechanism that shares the one instance across the steps, and the sharing is the reason the final submission includes every step’s values . The step validation is the trigger call with the specific field names, and the trigger is the mechanism that validates only the current step without the later steps’ errors.

Key point: The multi-step form is one useForm instance shared through the FormProvider. The step components call the useFormContext instead of the useForm. The shouldUnregister must stay false (the default), or the earlier steps’ values disappear when the step unmounts. The step validation is the trigger call with the current step’s field names, and the final handleSubmit validates every field.


Why the multi-step form needs the single instance

The separate-instance problem. A wizard that calls the useForm inside each step creates the separate form instances. The step 1’s instance holds the step 1’s values, and the step 2’s instance holds the step 2’s values, and the two do not know about each other. The final submission sends only the last step’s values, and the earlier steps’ values are the missing . The FormProvider is the mechanism that shares the one instance, and the sharing is the reason the final submission includes every step.

The unmount problem. The step change unmounts the previous step’s inputs. The React Hook Form’s default shouldUnregister: false keeps the unmounted fields’ values in the form state, and the default is the mechanism that preserves the values . The shouldUnregister: true is the option that removes the unmounted fields from the values object, and the removal is the reason the earlier steps’ values disappear . The default is the correct choice for the wizard, and the shouldUnregister: true is the choice for the conditional fields that must not submit.

The step-validation problem. The handleSubmit validates every field, and the every-field validation is the wrong trigger for the step’s “Next” button. The trigger with the current step’s field names is the mechanism that validates only the current step, and the validation is the reason the later steps’ errors do not block the earlier steps . The getFieldsForStep is the mechanism that maps the step number to the field names, and the mapping is the reason the trigger knows which fields to validate .

The draft problem. The wizard can be long, and the user can leave the page. The subscribe with the skipRender: true is the mechanism that stores the draft without the re-render, and the storage is the reason the draft survives the reload . The defaultValues from the store is the mechanism that restores the draft, and the restore is the reason the user does not start over.

The navigation problem. The next() and the back() are the navigation functions, and the currentStep is the state that the UI reads. The FormWizard from the react-form-step-wizard package is the mechanism that provides the navigation, or the useState with the step number is the manual alternative . The navigation is the mechanism that moves between the steps, and the step state is the reason the UI shows the one step at a time.


a. The single instance and the step components

The useForm is called once, at the top of the wizard, and the FormProvider spreads the methods to the step components. The step components call the useFormContext and read the register and the formState from the context.

// The single instance and the FormProvider
import { useForm, FormProvider, useFormContext } from "react-hook-form";

type WizardFormValues = {
  firstName: string;
  lastName: string;
  email: string;
  company: string;
};

function WizardForm() {
  // The single instance is created at the top of the wizard.
  // The shouldUnregister stays false (the default).
  const methods = useForm<WizardFormValues>({
    mode: "onBlur",
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
      company: "",
    },
  });

  return (
    // The FormProvider shares the methods with the step components.
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit((data) => console.log(data))}>
        <Step1 />
        <Step2 />
      </form>
    </FormProvider>
  );
}

function Step1() {
  // The useFormContext reads the methods from the provider.
  const {
    register,
    formState: { errors },
  } = useFormContext<WizardFormValues>();

  return (
    <div>
      <input {...register("firstName")} placeholder="First name" />
      {errors.firstName && <span>{errors.firstName.message}</span>}
      <input {...register("lastName")} placeholder="Last name" />
    </div>
  );
}

The useFormContext<WizardFormValues>() is the mechanism that reads the register and the formState from the provider. The step component does not accept the props, and the prop-less API is the reason the steps are clean . The FormProvider is placed around the <form> element, and the placement is the reason the provider scopes the form.

The separate useForm inside the step is the anti-pattern. The separate instance holds the separate values, and the final submission sends only the last step’s values . The FormProvider is the mechanism that avoids the anti-pattern, and the avoidance is the reason the wizard works.


b. The step validation and the navigation

The step validation is the trigger call with the current step’s field names. The getFieldsForStep maps the step number to the field names, and the trigger validates only those fields.

// The step validation
import { useState } from "react";

const STEP_FIELDS = {
  1: ["firstName", "lastName", "email"],
  2: ["company"],
} as const;

function WizardForm() {
  const [currentStep, setCurrentStep] = useState(1);
  const methods = useForm<WizardFormValues>({
    mode: "onBlur",
    defaultValues: { firstName: "", lastName: "", email: "", company: "" },
  });

  const handleNext = async () => {
    // The trigger validates only the current step's fields.
    // The later steps' errors do not block the earlier steps.
    const fields = STEP_FIELDS[currentStep as keyof typeof STEP_FIELDS];
    const isStepValid = await methods.trigger(fields);

    if (isStepValid) {
      setCurrentStep((prev) => Math.min(prev + 1, 2));
    }
  };

  const handleBack = () => {
    setCurrentStep((prev) => Math.max(prev - 1, 1));
  };

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit((data) => console.log(data))}>
        {currentStep === 1 && <Step1 />}
        {currentStep === 2 && <Step2 />}

        {currentStep > 1 && (
          <button type="button" onClick={handleBack}>Back</button>
        )}
        {currentStep < 2 ? (
          <button type="button" onClick={handleNext}>Next</button>
        ) : (
          <button type="submit">Submit</button>
        )}
      </form>
    </FormProvider>
  );
}

The methods.trigger(fields) is the mechanism that validates only the current step’s fields. The fields is the array of the field names from the STEP_FIELDS map, and the map is the mechanism that maps the step number to the fields . The isStepValid is the boolean that decides whether the step advances, and the decision is the reason the later steps’ errors do not block the earlier steps.

The shouldUnregister must stay false. The step change unmounts the previous step’s inputs, and the shouldUnregister: false is the mechanism that keeps the values in the form state . The shouldUnregister: true is the trap that deletes the values, and the deletion is the reason the final submission is missing the earlier steps .

The final handleSubmit validates every field, and the every-field validation is the mechanism that catches the earlier step’s error if the user somehow advanced without fixing it . The onInvalid handler is the mechanism that sends the user back to the step with the error, and the back-navigation is the reason the user can fix the error.


Complete Example Session

// ============================================
// PART 1: THE SINGLE INSTANCE
// ============================================

import { useState } from "react";
import { useForm, FormProvider } from "react-hook-form";

function WizardForm() {
  const [step, setStep] = useState(1);

  // The single instance for the whole wizard.
  // The shouldUnregister is false by default.
  const methods = useForm<WizardFormValues>({
    mode: "onBlur",
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
      company: "",
    },
  });

  return (
    <FormProvider {...methods}>
      <form onSubmit={methods.handleSubmit((data) => console.log(data))}>
        {step === 1 && <Step1 />}
        {step === 2 && <Step2 />}
      </form>
    </FormProvider>
  );
}

// ============================================
// PART 2: THE STEP COMPONENT
// ============================================

import { useFormContext } from "react-hook-form";

function Step1() {
  const { register, formState: { errors } } = useFormContext<WizardFormValues>();

  return (
    <div>
      <input {...register("firstName")} />
      {errors.firstName && <span>{errors.firstName.message}</span>}
      <input {...register("email")} />
    </div>
  );
}

// ============================================
// PART 3: THE STEP VALIDATION
// ============================================

const STEP_FIELDS = {
  1: ["firstName", "email"],
  2: ["company"],
};

const handleNext = async () => {
  const isStepValid = await methods.trigger(STEP_FIELDS[step]);
  if (isStepValid) setStep((s) => s + 1);
};

// The trigger validates only the current step's fields.

// ============================================
// PART 4: THE SHOULD UNREGISTER TRAP
// ============================================

// โŒ The shouldUnregister: true deletes the earlier steps.
useForm({ shouldUnregister: true });

// โœ… The default (false) keeps the values.
useForm();

// ============================================
// PART 5: THE FINAL SUBMISSION
// ============================================

const onSubmit = (data: WizardFormValues) => {
  // The data includes the values from every step.
  console.log(data);
};

// The handleSubmit validates every field.

// ============================================
// PART 6: THE DRAFT
// ============================================

// The subscribe with the skipRender stores the draft.
useEffect(() => {
  const subscription = methods.subscribe({
    formState: { values: true },
    callback: ({ values }) => saveDraft(values),
  });
  return () => subscription.unsubscribe();
}, [methods]);

// ============================================
// PART 7: THE DRAFT RESTORE
// ============================================

// The defaultValues from the store restores the draft.
useForm({ defaultValues: storedDraft ?? emptyWizard });

// ============================================
// PART 8: THE NAVIGATION PACKAGE
// ============================================

import { FormWizard, Step, useWizard } from "react-form-step-wizard";

// The FormWizard provides the next() and the back().
// The Step defines the one step at a time.

// ============================================
// PART 9: THE SEPARATE INSTANCE ANTI-PATTERN
// ============================================

// โŒ The useForm inside each step creates the separate instances.
// The final submission sends only the last step's values.

// โœ… The one useForm at the top and the useFormContext in the steps.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The useForm is called once at the top.
// [ ] The FormProvider spreads the methods.
// [ ] The step components call the useFormContext.
// [ ] The shouldUnregister is false.
// [ ] The trigger validates only the current step.
// [ ] The final handleSubmit validates every field.

The ten parts covered the single instance, the step component, the step validation, the shouldUnregister trap, the final submission, the draft, the draft restore, the navigation package, the separate-instance anti-pattern, and the checklist. The through-line is that the single instance is the state preservation, and the shouldUnregister: false is the mechanism that keeps the values.


Quick Reference

The single-instance pattern

StepMechanism
The form instanceThe one useForm at the top
The sharingThe FormProvider and the useFormContext
The step renderingThe conditional render by the step state
The step validationThe trigger with the step’s field names
The final validationThe handleSubmit on every field

The shouldUnregister values

ValueBehaviourUse for
false (default)The unmounted fields keep the valuesThe wizard
trueThe unmounted fields are removedThe conditional fields that must not submit

The step validation

FunctionPurpose
trigger(fields)Validate the specific fields
handleSubmitValidate every field
STEP_FIELDSThe step-to-fields map

The draft persistence

MechanismPurpose
subscribe with skipRenderStore the draft without the re-render
The defaultValues from the storeRestore the draft

Best Practices

โœ… Do This:

// Use the single useForm at the top
const methods = useForm<WizardFormValues>();                      // โœ…

// Share the methods through the FormProvider
<FormProvider {...methods}><form>...</form></FormProvider>        // โœ…

// Read the methods in the step components
const { register } = useFormContext<WizardFormValues>();          // โœ…

// Validate only the current step
await methods.trigger(STEP_FIELDS[step]);                          // โœ…

โŒ Don’t Do This:

// Use the useForm inside each step
function Step1() { const form = useForm(); } // the separate instances // โŒ

// Set the shouldUnregister to true
useForm({ shouldUnregister: true }); // the earlier steps are deleted // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The final submission is missing the earlier stepsThe shouldUnregister: true is setUse the default false
The step 2 values are missingThe separate useForm is used per stepUse the one useForm at the top
The later steps’ errors block the earlier stepsThe handleSubmit is used for the stepUse the trigger with the step fields
The draft is lost on reloadThe subscribe is missingAdd the subscribe and the defaultValues restore

Real-World Examples

1. The single instance

const methods = useForm<WizardFormValues>();

2. The FormProvider

<FormProvider {...methods}><form>...</form></FormProvider>

3. The useFormContext

const { register } = useFormContext<WizardFormValues>();

4. The step validation

await methods.trigger(["firstName", "email"])

5. The shouldUnregister default

useForm() // shouldUnregister is false by default

6. The final submission

methods.handleSubmit((data) => console.log(data))

7. The draft subscribe

methods.subscribe({ formState: { values: true }, callback: saveDraft })

8. The draft restore

useForm({ defaultValues: storedDraft })

9. The navigation package

<FormWizard onSubmit={onSubmit}><Step name="step1">...</Step></FormWizard>

10. The step state

const [step, setStep] = useState(1)

Visual

The Single Instance

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE SINGLE INSTANCE                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  The WizardForm                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ useForm             โ”‚  The one instance                  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚             โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ FormProvider        โ”‚  The context                       โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                                         โ”‚
โ”‚      โ–ผ             โ–ผ                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚Step1   โ”‚   โ”‚Step2   โ”‚  The useFormContext                โ”‚
โ”‚  โ”‚        โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The shouldUnregister Trap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE shouldUnregister TRAP                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  โœ… The default (false):                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The step 1 unmounts โ”‚  The values stay                   โ”‚
โ”‚  โ”‚ The step 2 mounts   โ”‚  The final submit includes them    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  โŒ The shouldUnregister: true:                               โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The step 1 unmounts โ”‚  The values are deleted            โ”‚
โ”‚  โ”‚ The step 2 mounts   โ”‚  The final submit is missing them  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Step Validation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE STEP VALIDATION                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  The Next button                                             โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ trigger(["email"])  โ”‚  The current step's fields         โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                                         โ”‚
โ”‚      โ–ผ             โ–ผ                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚Valid   โ”‚   โ”‚Invalid โ”‚                                    โ”‚
โ”‚  โ”‚        โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ”‚The nextโ”‚   โ”‚The     โ”‚                                    โ”‚
โ”‚  โ”‚step    โ”‚   โ”‚errors  โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
The instanceThe one useForm at the top
The sharingThe FormProvider and the useFormContext
The shouldUnregisterfalse (the default)
The step validationThe trigger with the step’s fields
The final validationThe handleSubmit on every field
The draftThe subscribe with the skipRender
The draft restoreThe defaultValues from the store

Key takeaways:

  • The multi-step form is one useForm instance. The FormProvider shares the instance with the step components, and the useFormContext reads it. The separate useForm per step creates the separate instances, and the final submission is missing the earlier steps .
  • The shouldUnregister must stay false. The step change unmounts the previous step’s inputs, and the default false keeps the values in the form state. The shouldUnregister: true is the trap that deletes the earlier steps .
  • The step validation is the trigger with the step’s fields. The trigger validates only the current step, and the later steps’ errors do not block the earlier steps . The final handleSubmit validates every field.
  • The draft is the subscribe and the defaultValues. The subscribe with the skipRender stores the values without the re-render, and the defaultValues from the store restores them .
  • The navigation is the step state or the package. The useState with the step number is the manual navigation, and the FormWizard from the react-form-step-wizard is the package alternative .

Remember: The React Hook Form multi-step form is the single instance and the FormProvider. The useForm is called once, the steps call the useFormContext, and the shouldUnregister stays false. The trigger validates the current step, and the handleSubmit validates every field. The state preservation is the mechanism that keeps the earlier steps’ values, and the default is the reason it works.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!