Replies: 2 comments 2 replies
-
I have a working solution, but it requires a bit of hackery. The ProblemIt would work quite easily if I could use the stored state from sessionRegistry in the initial rendering of the useForm hook. However, that causes a hydration error, because it was not available on the server side. Therefore, the state must be injected into the form after hydration. The tricky part is: The WorkaroundTo work around, this I use two hooks
The PersistedFormState provides a While writing this, I figure out the part with the Higher Order Component, and in the end, that makes it quite usable from a caller perspective. I don't like that I have to force a complete re-render, when a simple resetInitivalValues() method on the form would do just as well. But it works :) Usage:const PERSISTED_FORM_ID = "my-contact-form";
const ContactForm = withPersistedFormState(
PERSISTED_FORM_ID,
({ persistedFormState }: { persistedFormState: PersistedFormState }) => {
const { lastResult, startStep } = useActionData<typeof action>() ?? {};
const schema = createSchema(lastResult?.intent ?? null);
const [form] = usePersistedForm(persistedFormState, {
lastResult,
constraint: getZodConstraint(schema),
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: "onBlur",
shouldRevalidate: "onInput",
});
return (
<Form form={form} method="post">
<Button type="submit" className="hidden" {...form.validate.getButtonProps()} />
</Form>
);
},
);
export function clearPersistedContactFormState() {
return clearPersistedFormState(PERSISTED_FORM_ID);
}
export default function ContactFormPage() {
return (
<ContactForm />
);
} Implementationhelper functions// persisted-form.tsx (helper functions)
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useForm } from "@conform-to/react";
import { useEffect, useState } from "react";
import type { FormOptions } from "@conform-to/dom";
import type { SubmissionResult } from "@conform-to/react";
type FormState = SubmissionResult["initialValue"];
/**
* Save form data to sessionStorage
* @param formId Unique identifier for the form
* @param formState Form data to save
*/
function saveFormToSessionStorage(formId: string, formState: FormState): void {
if (typeof window === "undefined") return; // Skip on server-side
try {
sessionStorage.setItem(`form-${formId}`, JSON.stringify(formState));
} catch (error) {
console.error(`Error saving form data to sessionStorage: ${error}`);
}
}
/**
* Load form data from sessionStorage
* @param formId Unique identifier for the form
* @returns Form data or null if not found
*/
function loadFormFromSessionStorage(formId: string): FormState {
if (typeof window === "undefined") return null; // Skip on server-side
try {
const storedData = sessionStorage.getItem(`form-${formId}`);
return storedData ? JSON.parse(storedData) : null;
} catch (error) {
console.error(`Error loading form data from sessionStorage: ${error}`);
return null;
}
}
/**
* Clear form data from sessionStorage
* @param formId Unique identifier for the form
*/
export function clearPersistedFormState(formId: string): void {
if (typeof window === "undefined") return; // Skip on server-side
try {
sessionStorage.removeItem(`form-${formId}`);
} catch (error) {
console.error(`Error clearing form data from sessionStorage: ${error}`);
}
}
type OnValidate<Schema extends Record<string, any>, FormValue = Schema, FormError = string[]> = NonNullable<
FormOptions<Schema, FormValue, FormError>["onValidate"]
>; // persisted-form.tsx (interesting part)
export type PersistedFormState = {
key: string;
formId: string;
formState?: SubmissionResult<any> | undefined;
};
/**
* Retrieve the form state from sessionStorage.
*
* This hook should be used in the parent component of the form to ensure that the form is rerendered by passing the
* state's key when the form state is loaded.
* @param formId
*/
export function usePersistedFormState(formId: string): PersistedFormState {
const [initialFormState, setInitialFormStates] = useState<PersistedFormState>({ key: `unloaded-${formId}`, formId });
useEffect(() => {
const storedData = loadFormFromSessionStorage(formId);
// if we don't have stored data, there's no need to switch to loaded and cause a re-render
if (!storedData) return;
setInitialFormStates({
key: `loaded-${formId}`,
formId,
formState: { initialValue: storedData },
});
}, [formId]);
return initialFormState;
}
/**
* Custom hook for form persistence with sessionStorage
*/
export function usePersistedForm<Schema extends Record<string, any>, FormValue = Schema, FormError = string[]>(
persistedFormState: PersistedFormState,
options: Omit<Parameters<typeof useForm<Schema, FormValue, FormError>>[0], "onValidate"> & {
onValidate: OnValidate<Schema, FormError, FormValue>;
},
): ReturnType<typeof useForm<Schema, FormValue, FormError>> {
const { formId, formState } = persistedFormState;
const { lastResult: inputLastResult, onValidate: inputOnValidate } = options;
// Create a wrapper for onValidate function that stores form state in sessionStorage
const onValidate: OnValidate<Schema, FormError, FormValue> = (context) => {
const result = inputOnValidate(context);
const newLastResult = result.reply();
if (newLastResult.initialValue) {
saveFormToSessionStorage(formId, newLastResult.initialValue);
}
return result;
};
const lastResult = inputLastResult ?? formState;
return useForm({ ...options, onValidate, lastResult });
}
export function withPersistedFormState<T extends Record<string, unknown> = {}>(
formId: string,
Component: ComponentType<T & { persistedFormState: PersistedFormState }>,
): FC<T> {
const result: FC<T> = (props) => {
const persistedFormState = usePersistedFormState(formId);
return <Component {...props} key={persistedFormState.key} persistedFormState={persistedFormState} />;
};
result.displayName = `withPersistedFormState(${Component.displayName || Component.name || "Component"})`;
return result;
} |
Beta Was this translation helpful? Give feedback.
-
Are you using React Router or Next.js? If you are using Next.js, this library may solve your problem. https://github.com/recruit-tech/location-state |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I'm trying to store the current form state in sessionStorage, so the user can continue filling the form even if they navigate away.
I'm having trouble finding the right places to store and read the state. Has anyone attempted this successfully and can give me some pointers?
Beta Was this translation helpful? Give feedback.
All reactions