10 February 2026

How React Resets State, and Why It Might Surprise You

While building Formity, I ran into a problem that initially felt quite counterintuitive. When navigating to the next step, the form was still using the state from the previous step, as shown in the example below.

What is your mother's name?

This led me to question my implementation. I was convinced something was wrong, especially because it appeared that I was rendering two completely different forms, as shown in the code below.

// ...

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        name: ["", []],
      }),
      render: ({ values, ...props }) => (
        <FormStep
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              name: z.string().min(1, "Required"),
            }),
          )}
          question="What is your mother's name?"
          message={null}
          answer={{
            type: "shortText",
            name: "name",
            placeholder: "Enter your mother's name",
          }}
          nextButton="Ok"
          backButton={null}
          {...props}
        />
      ),
    },
  },
  {
    variables: ({ name }) => ({
      motherName: name,
    }),
  },
  {
    form: {
      values: () => ({
        name: ["", []],
      }),
      render: ({ values, ...props }) => (
        <FormStep
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              name: z.string().min(1, "Required"),
            }),
          )}
          question="What is your father's name?"
          message={null}
          answer={{
            type: "shortText",
            name: "name",
            placeholder: "Enter your father's name",
          }}
          nextButton="Ok"
          backButton={null}
          {...props}
        />
      ),
    },
  },
  {
    variables: ({ name }) => ({
      fatherName: name,
    }),
  },
  {
    return: ({ motherName, fatherName }) => ({
      motherName,
      fatherName,
    }),
  },
];

After diving deeper into how React works, I eventually realized where the issue was coming from. In this article, I’ll explain what I learned from this experience, with the goal of helping you avoid the same confusion if you run into a similar issue in your own projects.

How React Detects Changes

React doesn’t track changes directly in the DOM, as doing so would be inefficient. Instead, it relies on the Virtual DOM—an in-memory object tree that represents the structure of the UI. On every re-render, React creates a new Virtual DOM and compares it with the previous one to determine the minimal set of changes required to update the real DOM.

In this case, when moving to the next form step, React generated a Virtual DOM that still contained the same form component. Because React couldn’t identify it as a new instance, it reused the existing component and preserved its internal state, causing the form data to carry over unexpectedly.

The Key Prop

The solution was to explicitly tell React to treat each form step as a new component instance, which is exactly what the key prop allows us to do.

The key prop is a special identifier used by React to distinguish between component instances. When a component is rendered with a different key, React will unmount the previous instance and mount a new one, which resets its internal state and prevents data from being carried over between steps.

// ...

export const schema: Schema<Values> = [
  {
    form: {
      values: () => ({
        name: ["", []],
      }),
      render: ({ values, ...props }) => (
        <FormStep
          key="motherName"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              name: z.string().min(1, "Required"),
            }),
          )}
          question="What is your mother's name?"
          message={null}
          answer={{
            type: "shortText",
            name: "name",
            placeholder: "Enter your mother's name",
          }}
          nextButton="Ok"
          backButton={null}
          {...props}
        />
      ),
    },
  },
  {
    variables: ({ name }) => ({
      motherName: name,
    }),
  },
  {
    form: {
      values: () => ({
        name: ["", []],
      }),
      render: ({ values, ...props }) => (
        <FormStep
          key="fatherName"
          defaultValues={values}
          resolver={zodResolver(
            z.object({
              name: z.string().min(1, "Required"),
            }),
          )}
          question="What is your father's name?"
          message={null}
          answer={{
            type: "shortText",
            name: "name",
            placeholder: "Enter your father's name",
          }}
          nextButton="Ok"
          backButton={null}
          {...props}
        />
      ),
    },
  },
  {
    variables: ({ name }) => ({
      fatherName: name,
    }),
  },
  {
    return: ({ motherName, fatherName }) => ({
      motherName,
      fatherName,
    }),
  },
];

With this change in place, each form step is rendered independently, and the form behaves as expected.

What is your mother's name?