Skip to main content
New to FormEngine? It’s a React library that renders forms from JSON schemas — free, MIT-licensed. Start here →

What You’ll Learn

In this tutorial, you’ll learn how to pre-fill FormEngine forms with default values using the initialData prop. We’ll cover:
  • Setting static default values
  • Loading and pre-filling data from an API (edit forms)
  • Pre-filling from URL parameters
  • Saving and restoring form data with localStorage
  • Partial pre-fill (only some fields)
  • Common pitfalls and how to avoid them
Pre-filling is essential for edit forms, restoring saved drafts, and improving user experience by reducing data re-entry.

Basic: Static Default Values

The simplest way to pre-fill a form is with the initialData prop. Pass an object where keys match your JSON schema field keys:
import React from 'react';
import { FormViewer } from '@react-form-builder/core';
import { view } from '@react-form-builder/components-rsuite';

const form = {
  key: 'userForm',
  type: 'Screen',
  children: [
    {
      key: 'name',
      type: 'RsInput',
      props: {
        label: 'Full Name',
        placeholder: 'Enter your name'
      }
    },
    {
      key: 'email',
      type: 'RsInput',
      props: {
        label: 'Email Address',
        placeholder: 'Enter your email'
      }
    },
    {
      key: 'submitBtn',
      type: 'RsButton',
      props: {
        label: 'Submit',
        type: 'primary'
      }
    }
  ]
};

export default function MyForm() {
  // Pre-fill with default values
  const defaultValues = {
    name: 'John Doe',
    email: 'john@example.com'
  };

  return (
    <FormViewer
      view={view}
      getForm={async () => form}
      initialData={defaultValues}
    />
  );
}
When the form renders, the Name field will contain “John Doe” and the Email Address field will contain “john@example.com”. Key point: Only include keys in initialData that exist in your JSON schema. Extra keys are ignored.

Edit Form: Load Data from API

A common use case is pre-filling a form when editing an existing record. Load the data from an API and pass it as initialData:
import React, { useState, useEffect } from 'react';
import { FormViewer } from '@react-form-builder/core';
import { view } from '@react-form-builder/components-rsuite';

const form = {
  key: 'userForm',
  type: 'Screen',
  children: [
    {
      key: 'name',
      type: 'RsInput',
      props: { label: 'Full Name' }
    },
    {
      key: 'email',
      type: 'RsInput',
      props: { label: 'Email Address' }
    },
    {
      key: 'department',
      type: 'RsDropdown',
      props: {
        label: 'Department',
        data: [
          { label: 'Engineering', value: 'eng' },
          { label: 'Sales', value: 'sales' },
          { label: 'Support', value: 'support' }
        ]
      }
    },
    {
      key: 'submitBtn',
      type: 'RsButton',
      props: { label: 'Update Profile' }
    }
  ]
};

export default function EditUserForm({ userId }) {
  const [initialData, setInitialData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Fetch user data from API
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch user');
        const data = await response.json();
        setInitialData(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <FormViewer
      view={view}
      getForm={async () => form}
      initialData={initialData}
    />
  );
}
When the component mounts and the API call completes, the form populates with the user’s existing data. The user can then edit and submit the updated values.

Pre-fill from URL Parameters

Use React Router’s useSearchParams (or Next.js URL search params) to populate form fields from query parameters:
import React, { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { FormViewer } from '@react-form-builder/core';
import { view } from '@react-form-builder/components-rsuite';

const form = {
  key: 'contactForm',
  type: 'Screen',
  children: [
    {
      key: 'name',
      type: 'RsInput',
      props: { label: 'Your Name' }
    },
    {
      key: 'email',
      type: 'RsInput',
      props: { label: 'Your Email' }
    },
    {
      key: 'subject',
      type: 'RsInput',
      props: { label: 'Subject' }
    },
    {
      key: 'submitBtn',
      type: 'RsButton',
      props: { label: 'Send Message' }
    }
  ]
};

export default function ContactForm() {
  const [searchParams] = useSearchParams();

  const initialData = useMemo(() => {
    return {
      name: searchParams.get('name') || '',
      email: searchParams.get('email') || '',
      subject: searchParams.get('subject') || ''
    };
  }, [searchParams]);

  return (
    <FormViewer
      view={view}
      getForm={async () => form}
      initialData={initialData}
    />
  );
}

// Example URL: /contact?name=Jane&email=jane@example.com&subject=Help
This pattern is useful for referral links, shared forms, or passing context from another page.

Pre-fill from localStorage (Save Draft)

Allow users to save their progress and restore it on the next visit:
import React, { useState, useEffect } from 'react';
import { FormViewer } from '@react-form-builder/core';
import { view } from '@react-form-builder/components-rsuite';

const form = {
  key: 'surveyForm',
  type: 'Screen',
  children: [
    {
      key: 'q1',
      type: 'RsInput',
      props: { label: 'Question 1' }
    },
    {
      key: 'q2',
      type: 'RsInput',
      props: { label: 'Question 2' }
    },
    {
      key: 'submitBtn',
      type: 'RsButton',
      props: { label: 'Submit Survey' }
    }
  ]
};

const DRAFT_KEY = 'survey_draft';

export default function SurveyForm() {
  const [initialData, setInitialData] = useState(() => {
    // Load draft from localStorage on mount
    const saved = localStorage.getItem(DRAFT_KEY);
    return saved ? JSON.parse(saved) : {};
  });

  const [formRef] = useState({ current: null });

  // Save draft whenever form data changes
  const handleFormDataChange = ({ data, errors }) => {
    localStorage.setItem(DRAFT_KEY, JSON.stringify(data));
  };

  const handleSubmit = async () => {
    // After successful submit, clear the draft
    localStorage.removeItem(DRAFT_KEY);
    alert('Survey submitted!');
  };

  return (
    <>
      <FormViewer
        view={view}
        getForm={async () => form}
        initialData={initialData}
        viewerRef={formRef}
        onFormDataChange={handleFormDataChange}
      />
      <button onClick={handleSubmit} style={{ marginTop: '10px' }}>
        Submit & Clear Draft
      </button>
    </>
  );
}
This example:
  1. Loads the draft from localStorage when the component mounts (via useState initializer function)
  2. Saves the form data to localStorage every time the user makes a change
  3. Clears the draft after successful submission

Partial Pre-fill (Only Some Fields)

You don’t need to provide values for every field. Include only the fields you want to pre-fill:
const initialData = {
  name: 'Jane Smith',
  // email is omitted — it will remain empty
  // department is omitted — it will show the placeholder
};

return (
  <FormViewer
    view={view}
    getForm={async () => form}
    initialData={initialData}
  />
);
Fields not included in initialData will render empty or with their placeholder text. This is useful when you only have partial data.

What Happens When initialData Changes After Mount?

Important: The initialData prop is applied only when the form first mounts. If you change initialData after the form is already rendered, the form will NOT automatically update to the new values.
// ❌ This won't work as expected
export default function MyForm() {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    setTimeout(() => {
      setUserData({ name: 'Updated Name', email: 'new@example.com' });
    }, 2000);
  }, []);

  return (
    <FormViewer
      view={view}
      getForm={async () => form}
      initialData={userData} // Changes after mount — form won't update
    />
  );
}
Solution: If you need to update form data after mount, use the viewerRef to programmatically set the data:
export default function MyForm() {
  const [userData, setUserData] = useState(null);
  const formRef = useRef(null);

  useEffect(() => {
    setTimeout(() => {
      const newData = { name: 'Updated Name', email: 'new@example.com' };
      setUserData(newData);
      // Update form data directly via ref
      if (formRef.current) {
        formRef.current.formData = newData;
      }
    }, 2000);
  }, []);

  return (
    <FormViewer
      view={view}
      getForm={async () => form}
      initialData={userData}
      viewerRef={formRef}
    />
  );
}
Or, if you need the form to respond to changes, remount it with a new key:
export default function MyForm() {
  const [userData, setUserData] = useState({ name: 'John' });
  const [formKey, setFormKey] = useState(0);

  const handleUpdateData = (newData) => {
    setUserData(newData);
    setFormKey(k => k + 1); // Force remount, form re-initializes with new data
  };

  return (
    <FormViewer
      key={formKey}
      view={view}
      getForm={async () => form}
      initialData={userData}
    />
  );
}

Common Mistakes

1. Field Keys Don’t Match the Schema

If your JSON schema has a field with key firstName but you provide initialData={{ firstName: 'John' }}, it works. But if you use initialData={{ first_name: 'John' }}, the field won’t be pre-filled because the key doesn’t match.
// ✅ Correct
const form = {
  children: [
    { key: 'firstName', type: 'RsInput', ... }
  ]
};
const initialData = { firstName: 'John' };

// ❌ Wrong
const initialData = { first_name: 'John' }; // Key mismatch

2. Undefined vs Empty String

undefined and empty string '' are treated differently. Use empty strings for “no value,” not undefined:
// ✅ Better
const initialData = {
  name: 'John',
  email: '', // Explicitly empty
  phone: '' // Explicitly empty
};

// ❌ Avoid
const initialData = {
  name: 'John',
  email: undefined, // May behave unexpectedly
  phone: null // May behave unexpectedly
};

3. Assuming initialData Persists After Changes

initialData is just the starting point. When the user edits a field, the form’s internal state updates, but initialData doesn’t change. Don’t rely on initialData to reflect the current form state—use onFormDataChange callback or viewerRef.current.formData instead.
// ❌ Wrong
console.log(initialData); // Still shows original values, not current form state

// ✅ Right
const handleFormDataChange = ({ data, errors }) => {
  console.log(data); // Current form state
};

4. Not Handling Async Data Load

If you’re loading initial data from an API, the form might render before the data arrives. Show a loading state or provide an empty initialData:
// ❌ Form renders before data loads
return <FormViewer initialData={userData} ... />;

// ✅ Handle loading state
if (loading) return <div>Loading...</div>;
return <FormViewer initialData={userData || {}} ... />;

Next Steps

Now that you can pre-fill forms, explore these related topics:
Last modified on April 16, 2026