Skip to main content

Autosave with Dynamic Fields

Forms with dynamic fields (fields that can be added or removed) require special handling for autosave. This guide shows you how to implement it.

Basic Dynamic Fields

import { useState } from 'react';
import { useFormAutosave } from '@form-guardian/react';

interface DynamicField {
id: string;
value: string;
}

function DynamicFieldsForm() {
const [fields, setFields] = useState<DynamicField[]>([
{ id: '1', value: '' },
]);

const { formRef, clearDraft } = useFormAutosave('dynamic-fields-form', {
autoRestore: true,
});

const addField = () => {
setFields([...fields, { id: Date.now().toString(), value: '' }]);
};

const removeField = (id: string) => {
setFields(fields.filter(f => f.id !== id));
};

const updateField = (id: string, value: string) => {
setFields(fields.map(f => f.id === id ? { ...f, value } : f));
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Submit logic
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit}>
{fields.map((field) => (
<div key={field.id}>
<input
name={`field-${field.id}`}
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
/>
<button type="button" onClick={() => removeField(field.id)}>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>Add Field</button>
<button type="submit">Submit</button>
</form>
);
}

With React Hook Form (useFieldArray)

React Hook Form's useFieldArray works great with Form Guardian:

import { useForm, useFieldArray } from 'react-hook-form';
import { useFormAutosave } from '@form-guardian/react';
import { useEffect } from 'react';

interface FormData {
items: { name: string; description: string }[];
}

function DynamicFieldsWithRHF() {
const { register, control, handleSubmit, setValue, getValues } = useForm<FormData>({
defaultValues: {
items: [{ name: '', description: '' }],
},
});

const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});

const { formRef, restoreValues, clearDraft } = useFormAutosave(
'dynamic-fields-form',
{ autoRestore: false }
);

// Restore draft on mount
useEffect(() => {
restoreValues(
(field, value) => {
// Handle dynamic field restoration
if (field.startsWith('items.')) {
const [index, subField] = field.split('.').slice(1);
const numIndex = parseInt(index);
if (!isNaN(numIndex)) {
// Ensure array has enough items
while (fields.length <= numIndex) {
append({ name: '', description: '' });
}
setValue(`items.${numIndex}.${subField}` as any, value, {
shouldValidate: false,
});
}
} else {
setValue(field as any, value, { shouldValidate: false });
}
},
() => getValues()
);
}, []);

const onSubmit = async (data: FormData) => {
await submitForm(data);
await clearDraft();
};

return (
<form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`items.${index}.name` as const, { required: true })}
placeholder="Name"
/>
<input
{...register(`items.${index}.description` as const)}
placeholder="Description"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', description: '' })}>
Add Item
</button>
<button type="submit">Submit</button>
</form>
);
}

Array of Items (Todo List Example)

function TodoListForm() {
const [todos, setTodos] = useState<{ id: string; text: string }[]>([]);

const { formRef, clearDraft } = useFormAutosave('todo-list', {
autoRestore: true,
});

const addTodo = () => {
setTodos([...todos, { id: Date.now().toString(), text: '' }]);
};

const removeTodo = (id: string) => {
setTodos(todos.filter(t => t.id !== id));
};

const updateTodo = (id: string, text: string) => {
setTodos(todos.map(t => t.id === id ? { ...t, text } : t));
};

return (
<form ref={formRef}>
{todos.map((todo) => (
<div key={todo.id}>
<input
name={`todo-${todo.id}`}
value={todo.text}
onChange={(e) => updateTodo(todo.id, e.target.value)}
placeholder="Todo item"
/>
<button type="button" onClick={() => removeTodo(todo.id)}>
Remove
</button>
</div>
))}
<button type="button" onClick={addTodo}>Add Todo</button>
</form>
);
}

Nested Dynamic Fields

For complex nested structures:

interface NestedFormData {
sections: {
title: string;
fields: { label: string; value: string }[];
}[];
}

function NestedDynamicFields() {
const [sections, setSections] = useState<NestedFormData['sections']>([
{ title: '', fields: [{ label: '', value: '' }] },
]);

const { formRef, clearDraft } = useFormAutosave('nested-dynamic-form', {
autoRestore: true,
});

const addSection = () => {
setSections([...sections, { title: '', fields: [{ label: '', value: '' }] }]);
};

const addField = (sectionIndex: number) => {
const newSections = [...sections];
newSections[sectionIndex].fields.push({ label: '', value: '' });
setSections(newSections);
};

return (
<form ref={formRef}>
{sections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<input
name={`section-${sectionIndex}-title`}
value={section.title}
onChange={(e) => {
const newSections = [...sections];
newSections[sectionIndex].title = e.target.value;
setSections(newSections);
}}
placeholder="Section title"
/>
{section.fields.map((field, fieldIndex) => (
<div key={fieldIndex}>
<input
name={`section-${sectionIndex}-field-${fieldIndex}-label`}
value={field.label}
onChange={(e) => {
const newSections = [...sections];
newSections[sectionIndex].fields[fieldIndex].label = e.target.value;
setSections(newSections);
}}
placeholder="Label"
/>
<input
name={`section-${sectionIndex}-field-${fieldIndex}-value`}
value={field.value}
onChange={(e) => {
const newSections = [...sections];
newSections[sectionIndex].fields[fieldIndex].value = e.target.value;
setSections(newSections);
}}
placeholder="Value"
/>
</div>
))}
<button type="button" onClick={() => addField(sectionIndex)}>
Add Field
</button>
</div>
))}
<button type="button" onClick={addSection}>Add Section</button>
</form>
);
}

Best Practices

  1. Use unique field names - Include IDs in field names for dynamic fields
  2. Handle restoration carefully - Ensure arrays are properly initialized before restoring
  3. Clear drafts after submission - Prevent stale data
  4. Use appropriate TTL - Dynamic forms may take longer to complete

Common Pitfalls

❌ Don't Use Array Indices as Keys

// ❌ Wrong - indices change when items are removed
{fields.map((field, index) => (
<input key={index} name={`field-${index}`} />
))}

✅ Use Stable IDs

// ✅ Correct - stable IDs don't change
{fields.map((field) => (
<input key={field.id} name={`field-${field.id}`} />
))}

Next Steps