PreserveBoundary
The
PreserveBoundarycomponent is part of Conform's future export. These APIs are experimental and may change in minor versions. Learn more
A React component that preserves form field values during client-side navigation when React unmounts its contents. Useful for multi-step wizards, form dialogs, and virtualized lists where fields are temporarily hidden but should still be included in form submission.
1import { PreserveBoundary } from '@conform-to/react/future';
2
3{
4 step === 1 ? (
5 <PreserveBoundary name="step-1">
6 <input name="name" />
7 <input name="email" />
8 </PreserveBoundary>
9 ) : step === 2 ? (
10 <PreserveBoundary name="step-2">
11 <input name="address" />
12 <input name="city" />
13 </PreserveBoundary>
14 ) : null;
15}#Props
name: string
A unique name for the boundary within the form. This is used to:
- Ensure proper unmount/remount behavior when switching between boundaries in conditional rendering
- Isolate preserved inputs so they don't conflict with inputs from other boundaries
form?: string
The id of the form to associate with. Only needed when the boundary is rendered outside the form element:
1<form id="my-form">
2 <button type="submit">Submit</button>
3</form>;
4{
5 /* Boundary outside the form */
6}
7<PreserveBoundary name="external" form="my-form">
8 <input name="field" />
9</PreserveBoundary>;#Examples
Multi-step form
1import { useForm, PreserveBoundary } from '@conform-to/react/future';
2import { useState } from 'react';
3
4function MultiStepForm() {
5 const [step, setStep] = useState(1);
6 const { form, fields } = useForm();
7
8 return (
9 <form {...form.props}>
10 {step === 1 ? (
11 <PreserveBoundary name="step-1">
12 <label>
13 Name
14 <input
15 name={fields.name.name}
16 defaultValue={fields.name.defaultValue}
17 />
18 </label>
19 <label>
20 Email
21 <input
22 name={fields.email.name}
23 defaultValue={fields.email.defaultValue}
24 />
25 </label>
26 </PreserveBoundary>
27 ) : step === 2 ? (
28 <PreserveBoundary name="step-2">
29 <label>
30 Address
31 <input
32 name={fields.address.name}
33 defaultValue={fields.address.defaultValue}
34 />
35 </label>
36 <label>
37 City
38 <input
39 name={fields.city.name}
40 defaultValue={fields.city.defaultValue}
41 />
42 </label>
43 </PreserveBoundary>
44 ) : null}
45
46 <div>
47 {step > 1 ? (
48 <button type="button" onClick={() => setStep(step - 1)}>
49 Previous
50 </button>
51 ) : null}
52 {step < 2 ? (
53 <button type="button" onClick={() => setStep(step + 1)}>
54 Next
55 </button>
56 ) : step === 2 ? (
57 <button type="submit">Submit</button>
58 ) : null}
59 </div>
60 </form>
61 );
62}Virtualized list
For virtualized lists, each item should use name with a stable identifier to ensure values are correctly associated with their items:
1import { useForm, PreserveBoundary } from '@conform-to/react/future';
2import { useVirtualizer } from '@tanstack/react-virtual';
3
4function VirtualizedItemList({ items }) {
5 const { form, fields } = useForm({
6 defaultValue: { items },
7 });
8
9 const parentRef = useRef(null);
10 const virtualizer = useVirtualizer({
11 count: items.length,
12 getScrollElement: () => parentRef.current,
13 estimateSize: () => 50,
14 });
15
16 const itemFields = fields.items.getFieldList();
17
18 return (
19 <form {...form.props}>
20 <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
21 <div
22 style={{ height: virtualizer.getTotalSize(), position: 'relative' }}
23 >
24 {virtualizer.getVirtualItems().map((virtualRow) => {
25 const itemField = itemFields[virtualRow.index];
26 return (
27 <div
28 key={virtualRow.key}
29 style={{
30 position: 'absolute',
31 top: virtualRow.start,
32 height: virtualRow.size,
33 }}
34 >
35 <PreserveBoundary name={`item-${virtualRow.index}`}>
36 <input
37 name={itemField.name}
38 defaultValue={itemField.defaultValue}
39 />
40 </PreserveBoundary>
41 </div>
42 );
43 })}
44 </div>
45 </div>
46 <button type="submit">Save All</button>
47 </form>
48 );
49}#Tips
Only use for navigational conditions
The condition that unmounts a PreserveBoundary should be navigational (step changes, dialog open/close), not when the user is intentionally excluding data from the submission. Otherwise, the preserved values will still be submitted even when the user hides the field:
1// Don't do this: the discount code will still be submitted even when hidden
2{
3 hasDiscountCode && (
4 <PreserveBoundary name="discount">
5 <input name="discountCode" />
6 </PreserveBoundary>
7 );
8}
9
10// Do this instead: let the field unmount normally
11{
12 hasDiscountCode && <input name="discountCode" />;
13}Stale values are cleaned up automatically
When the fields inside a boundary change based on external state, stale preserved values are automatically removed on remount. For example, if the user fills in companyName/jobTitle on step 2, goes back to step 1, and switches account type to "personal", the preserved business fields are removed when step 2 remounts:
1{
2 step === 1 ? (
3 <PreserveBoundary name="step-1">
4 <select name="accountType">
5 <option value="personal">Personal</option>
6 <option value="business">Business</option>
7 </select>
8 </PreserveBoundary>
9 ) : step === 2 ? (
10 <PreserveBoundary name="step-2">
11 {accountType === 'personal' ? (
12 <input name="dateOfBirth" type="date" />
13 ) : accountType === 'business' ? (
14 <>
15 <input name="companyName" />
16 <input name="jobTitle" />
17 </>
18 ) : null}
19 </PreserveBoundary>
20 ) : null;
21}