import { useCallback, useEffect, useState } from 'react'
import { field, useFormBridge } from '@runilib/react-formbridge'
const STORAGE_PREFIX = 'react-formbridge:'
const PERSIST_KEY = 'docs:persist:local-profile'
const STORAGE_KEY = STORAGE_PREFIX + PERSIST_KEY
const EMPTY_VALUES = {
fullName: '',
email: '',
notes: '',
newsletter: true,
otp: '',
}
const schema = {
fullName: field.text('Full name').required(),
email: field.email('Email').required(),
notes: field.textarea('Notes').required().min(10, 'Write at least 10 characters.'),
newsletter: field.checkbox('Email me product updates'),
otp: field.text('One-time code').placeholder('Excluded from persistence'),
}
function LocalDraftForm({
onRefreshStorage,
onRemount,
onRestore,
onSubmitted,
}: {
onRefreshStorage: () => void
onRemount: () => void
onRestore: () => void
onSubmitted: (values: Record<string, unknown>) => void
}) {
const form = useFormBridge(schema, {
validateOn: 'onBlur',
revalidateOn: 'onChange',
initialValues: {
newsletter: true,
},
persist: {
key: PERSIST_KEY,
storage: 'local',
ttl: 5 * 60,
debounce: 300,
exclude: ['otp'],
version: 'docs-local-v1',
onRestore: () => {
onRestore()
onRefreshStorage()
},
},
})
const { Form, fields, persistanceHelpers, state } = form
useEffect(() => {
if (typeof window === 'undefined') return undefined
const timer = window.setTimeout(() => {
onRefreshStorage()
}, 350)
return () => window.clearTimeout(timer)
}, [onRefreshStorage, state.values])
return (
<>
<Form
onSubmit={async (values) => {
onSubmitted(values)
}}
>
<div style={{ display: 'grid', gap: 12 }}>
<fields.fullName />
<fields.email />
<fields.notes />
<fields.newsletter />
<fields.otp />
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button
type="button"
onClick={async () => {
await persistanceHelpers.saveDraftNow()
onRefreshStorage()
}}
>
Save draft now
</button>
<button
type="button"
onClick={async () => {
await persistanceHelpers.saveDraftNow()
onRefreshStorage()
onRemount()
}}
>
Save + remount
</button>
<button
type="button"
onClick={async () => {
await persistanceHelpers.clearDraft()
onRefreshStorage()
}}
>
Clear saved draft
</button>
<button
type="button"
onClick={() => {
form.resetFields(EMPTY_VALUES)
}}
>
Reset fields only
</button>
</div>
<Form.Submit>Submit values</Form.Submit>
</div>
</Form>
<div
style={{
border: '1px solid #d6d9e0',
borderRadius: 12,
padding: 12,
background: '#fff',
marginTop: 16,
}}
>
<strong>Runtime snapshot</strong>
<p style={{ margin: '8px 0', color: '#4b5563', fontSize: 13 }}>
Loading draft: {String(persistanceHelpers.isLoadingDraft)}
{' · '}
Restored on mount: {String(persistanceHelpers.hasDraft)}
</p>
<pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(state.values, null, 2)}
</pre>
</div>
</>
)
}
export function PersistenceLocalStoragePlayground() {
const [instanceId, setInstanceId] = useState(0)
const [restoreCount, setRestoreCount] = useState(0)
const [rawDraft, setRawDraft] = useState<string | null>(null)
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null)
const refreshStorage = useCallback(() => {
if (typeof window === 'undefined') return
setRawDraft(window.localStorage.getItem(STORAGE_KEY))
}, [])
useEffect(() => {
refreshStorage()
}, [instanceId, refreshStorage])
return (
<div
style={{
fontFamily: 'sans-serif',
padding: 20,
background: '#f5f7fb',
display: 'grid',
gap: 16,
}}
>
<div>
<h3 style={{ margin: '0 0 8px' }}>Interactive localStorage persistence</h3>
<p style={{ margin: 0, color: '#4b5563' }}>
Type into the form, click <strong>Save + remount</strong>, and the same
draft is restored from browser local storage. The OTP field is excluded
on purpose.
</p>
</div>
<LocalDraftForm
key={instanceId}
onRefreshStorage={refreshStorage}
onRemount={() => setInstanceId((current) => current + 1)}
onRestore={() => setRestoreCount((current) => current + 1)}
onSubmitted={setSubmitted}
/>
<div
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1fr)',
gap: 12,
}}
>
<div
style={{
border: '1px solid #d6d9e0',
borderRadius: 12,
padding: 12,
background: '#fff',
}}
>
<strong>Persist configuration</strong>
<p style={{ margin: '8px 0', color: '#4b5563', fontSize: 13 }}>
Key: <code>{STORAGE_KEY}</code>
</p>
<p style={{ margin: '8px 0', color: '#4b5563', fontSize: 13 }}>
TTL: 5 minutes
{' · '}
Debounce: 300ms
{' · '}
Version: docs-local-v1
</p>
<p style={{ margin: 0, color: '#4b5563', fontSize: 13 }}>
Restores observed in this session: <strong>{restoreCount}</strong>
</p>
</div>
<div
style={{
border: '1px solid #d6d9e0',
borderRadius: 12,
padding: 12,
background: '#fff',
}}
>
<strong>Last submit</strong>
<pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(submitted, null, 2)}
</pre>
</div>
</div>
<div
style={{
border: '1px solid #d6d9e0',
borderRadius: 12,
padding: 12,
background: '#fff',
}}
>
<strong>Raw localStorage draft</strong>
<pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{rawDraft ?? 'No saved draft yet.'}
</pre>
</div>
</div>
)
}
export default PersistenceLocalStoragePlayground