feat(#15): fieldsNeedingReentry — close the excludeFields UX gap (v0.3.0)#16
Merged
Conversation
…try after restore
When `excludeFields: ['password']` is set in a multi-step wizard, the
user fills the password in step 1 and progresses to step 5. `step` is
persisted, password is not. On refresh, the wizard restores at step 5
with an empty password and the user clicks Submit — server gets an
empty password. Silent UX failure.
`fieldsNeedingReentry` solves this:
const draft = useFormDraft({
excludeFields: ['password'],
// ...
});
return (
<>
{draft.fieldsNeedingReentry.includes('password') && (
<Banner>보안을 위해 비밀번호를 다시 입력해주세요.</Banner>
)}
{/* form */}
</>
);
How it works:
- Stored record gains optional `__excludedHad?: string[]` — key names of
excludeFields whose values were non-default at persist time. Values
themselves are never persisted (that's the point of excludeFields).
- On restore, the hint hydrates internal state. The derived
`fieldsNeedingReentry` per-render filters to keys that are STILL at
default AND still in current `excludeFields`. As the user re-enters
a value, it falls out of the list automatically. Discard / submit /
conflict-resolve-remote clear the state.
- Pre-v0.3 records (without `__excludedHad`) restore cleanly with an
empty list — fully backward compatible.
Exposed on:
- `FormDraftResult.fieldsNeedingReentry`
- `useFormDraftStatus(key)` (sibling reader)
- `getFormDraft(key).getFieldsNeedingReentry()` (imperative)
- RHF / Formik / TanStack adapter returns
Audit-driven hardening:
- Restore filters non-string entries from `__excludedHad` (F4)
- Migrate path recomputes the hint against migrated values + current
defaults instead of carrying over stale field names (F5)
- useMemo intersects with current `excludeFields` so a shrinking set
doesn't surface ghost entries (F6)
- `resolveConflict('remote' | merged)` + multiTab `last-writer-wins`
remote/merged paths clear the hint (F2)
- Added a notify-effect on `fieldsNeedingReentry` so sibling
`useFormDraftStatus` consumers re-render on transitions without
waiting for a save/status change (F1)
Tests: 216 unit (was 202; +14) + 87 e2e (was 84; +3 cross-browser S8).
Bundle: 5.8 KB brotli / 8 KB CI gate.
Closes #15.
Extends the existing wizard demo (fill → refresh → values survive) with the new v0.3 `fieldsNeedingReentry` flow at the end: refresh shows the re-entry banner, going back to step 1 reveals the empty password field, typing it clears the banner. Recorded at 900x640, scaled to 800px wide, 14fps, brotli-friendly palette quantization. Size 1.2 MB (was 0.8 MB; longer because the reentry segment is appended).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #15.
Problem
`excludeFields: ['password']` keeps sensitive values out of storage, which is correct from a security standpoint — but creates a silent UX failure for multi-step wizards. The user fills password in step 1, progresses to step 5, refreshes. `step` is persisted, password is not. They land back at step 5 with an empty password field and click Submit, and the server gets an empty password. Nothing surfaces the gap.
API
```tsx
const draft = useFormDraft({
defaultValues: { email: '', password: '', step: 1 },
excludeFields: ['password'],
// ...
});
return (
<>
{draft.fieldsNeedingReentry.includes('password') && (
보안을 위해 비밀번호를 다시 입력해주세요.
)}
{/* form */}
</>
);
```
Also exposed on:
How it works
Audit round (one pass, all findings addressed)
Code review + adversarial audit ran in parallel after the initial implementation. Findings:
Each fix has a regression test.
Tests
Test plan
Migration
Drop-in for v0.2 consumers. No breaking changes. New API is opt-in via reading `draft.fieldsNeedingReentry`.
Bumps `package.json` to 0.3.0.