Conversation
There was a problem hiding this comment.
Pull Request Overview
Adds React 18 useSyncExternalStore–compatible APIs to Final Form, including type definitions, implementation, tests, examples, and documentation to avoid React setState warnings during render.
- Introduces
subscribeFieldState/getFieldSnapshotandsubscribeFormState/getFormSnapshotto the public API - Implements these new methods in
createFormand extends the TypeScript definitions - Adds comprehensive tests, an example hook, and docs for integrating with
React.useSyncExternalStore
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Added subscribeFieldState, getFieldSnapshot, subscribeFormState, and getFormSnapshot to the FormApi interface |
| src/FinalForm.ts | Implemented the new useSyncExternalStore–compatible methods |
| src/FinalForm.useSyncExternalStore.test.ts | Added tests covering snapshots, subscriptions, multiple subscribers, cleanup, and React integration |
| src/debug.test.ts | Added basic debug tests (currently using console.log) |
| examples/react-useSyncExternalStore.js | Provided an example React hook and component using the new API |
| docs/useSyncExternalStore.md | Documentation for the React integration and usage examples |
Comments suppressed due to low confidence (2)
src/debug.test.ts:11
- This test uses console.log instead of assertions; it won’t catch regressions. Replace these logs with
expectstatements to validate the callback invocation counts.
console.log("After registration, callback called:", fieldCallback.mock.calls.length);
src/debug.test.ts:16
- This log doesn’t verify behavior in the test. Use
expect(fieldCallback).toHaveBeenCalledTimes(...)instead of console.log to enforce correctness.
console.log("After change, callback called:", fieldCallback.mock.calls.length);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
Adds React.useSyncExternalStore-compatible methods to the Form API for better integration with React 18+, including types, implementation, tests, documentation, and examples.
- Extends
FormApitypes withsubscribeFieldState,getFieldSnapshot,subscribeFormState, andgetFormSnapshot. - Implements the new APIs in
src/FinalForm.tsby wrapping existing subscription mechanisms. - Updates tests, examples, and docs to cover and demonstrate the new hooks.
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Declare and document new useSyncExternalStore-compatible API methods in FormApi. |
| src/FinalForm.ts | Implement subscribeFieldState, getFieldSnapshot, subscribeFormState, getFormSnapshot. |
| src/debug.test.ts | Add basic debug test file (currently logs without assertions). |
| src/FinalForm.useSyncExternalStore.test.ts | Add comprehensive tests for new APIs ensuring React.useSyncExternalStore compatibility. |
| examples/react-useSyncExternalStore.js | Provide example React hooks and components using useSyncExternalStore with Final Form. |
| docs/useSyncExternalStore.md | Document the integration, API usage, and migration steps for React.useSyncExternalStore. |
Comments suppressed due to low confidence (2)
src/debug.test.ts:11
- This test logs callback calls but does not assert expected behavior. Please replace these
console.logcalls withexpectstatements to verifyfieldCallbackis called the correct number of times.
console.log("After registration, callback called:", fieldCallback.mock.calls.length);
src/debug.test.ts:16
- Similarly, add an assertion (e.g.,
expect(fieldCallback).toHaveBeenCalledTimes(...)) after this line to confirm the callback invocation rather than relying on aconsole.log.
console.log("After change, callback called:", fieldCallback.mock.calls.length);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
This PR introduces first-class support for React’s useSyncExternalStore by adding new APIs and updating types, implementations, docs, examples, and tests accordingly.
- Defines
subscribeFieldState/getFieldSnapshotandsubscribeFormState/getFormSnapshotin both types andcreateFormto align with the React 18+ external store pattern - Adds comprehensive tests (
FinalForm.useSyncExternalStore.test.ts) validating the new APIs and edge cases - Updates examples and documentation to showcase React hooks built on the new APIs
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.ts | Added type signatures and JSDoc for the new external-store APIs |
| src/FinalForm.ts | Implemented subscribeFieldState, getFieldSnapshot, subscribeFormState, getFormSnapshot |
| src/debug.test.ts | Initial debug tests added (currently use console.logs) |
| src/FinalForm.useSyncExternalStore.test.ts | Tests for field/form subscribe + snapshot via useSyncExternalStore pattern |
| examples/react-useSyncExternalStore.js | Example React hooks demonstrating both new and legacy subscription patterns |
| docs/useSyncExternalStore.md | Documentation for the new APIs and example usage |
Comments suppressed due to low confidence (4)
src/debug.test.ts:11
- Replace this console.log with a proper assertion, e.g.,
expect(fieldCallback).toHaveBeenCalledTimes(1)to verify behavior.
console.log("After registration, callback called:", fieldCallback.mock.calls.length);
src/debug.test.ts:16
- Remove the console.log and add an assertion like
expect(fieldCallback).toHaveBeenCalledTimes(2)to validate the callback was invoked on change.
console.log("After change, callback called:", fieldCallback.mock.calls.length);
src/debug.test.ts:28
- Swap this console.log for an assertion such as
expect(formCallback).toHaveBeenCalledTimes(1)to ensure the subscription triggers immediately.
console.log("After subscription, callback called:", formCallback.mock.calls.length);
src/debug.test.ts:33
- Replace the console.log with
expect(formCallback).toHaveBeenCalledTimes(2)to verify form callback invocation after change.
console.log("After change, callback called:", formCallback.mock.calls.length);
| (onChange) => form.subscribe(() => onChange(), subscription), | ||
| () => { | ||
| const state = form.getState(); | ||
| // Filter state based on subscription | ||
| const result = {}; | ||
| Object.keys(subscription).forEach((key) => { | ||
| if (subscription[key]) { | ||
| result[key] = state[key]; | ||
| } | ||
| }); | ||
| return result; |
There was a problem hiding this comment.
This generic hook uses form.subscribe and form.getState. To leverage the new external-store API, consider using form.subscribeFormState(onChange, subscription) and form.getFormSnapshot() instead.
| (onChange) => form.subscribe(() => onChange(), subscription), | |
| () => { | |
| const state = form.getState(); | |
| // Filter state based on subscription | |
| const result = {}; | |
| Object.keys(subscription).forEach((key) => { | |
| if (subscription[key]) { | |
| result[key] = state[key]; | |
| } | |
| }); | |
| return result; | |
| (onChange) => | |
| form.subscribeFormState(() => { | |
| const snapshot = form.getFormSnapshot(); | |
| const filteredSnapshot = {}; | |
| Object.keys(subscription).forEach((key) => { | |
| if (subscription[key]) { | |
| filteredSnapshot[key] = snapshot[key]; | |
| } | |
| }); | |
| onChange(filteredSnapshot); | |
| }, subscription), | |
| () => { | |
| const snapshot = form.getFormSnapshot(); | |
| const filteredSnapshot = {}; | |
| Object.keys(subscription).forEach((key) => { | |
| if (subscription[key]) { | |
| filteredSnapshot[key] = snapshot[key]; | |
| } | |
| }); | |
| return filteredSnapshot; |
| (onChange) => form.registerField(fieldName, () => onChange(), subscription), | ||
| () => { | ||
| const fieldState = form.getFieldState(fieldName); |
There was a problem hiding this comment.
Update this generic field hook to use form.subscribeFieldState(fieldName, onChange, subscription) and form.getFieldSnapshot(fieldName) instead of the legacy registerField/getFieldState.
| (onChange) => form.registerField(fieldName, () => onChange(), subscription), | |
| () => { | |
| const fieldState = form.getFieldState(fieldName); | |
| (onChange) => form.subscribeFieldState(fieldName, onChange, subscription), | |
| () => { | |
| const fieldState = form.getFieldSnapshot(fieldName); |
| (onChange) => form.subscribe(() => onChange(), subscription), | ||
| () => { | ||
| const state = form.getState(); | ||
| // Filter state based on subscription | ||
| const result = {}; | ||
| Object.keys(subscription).forEach((key) => { | ||
| if (subscription[key]) { | ||
| result[key] = state[key]; | ||
| } | ||
| }); | ||
| return result; | ||
| }, |
There was a problem hiding this comment.
In this example, useFormSubscription uses the legacy form.subscribe. For React 18+ integration, demonstrate form.subscribeFormState and form.getFormSnapshot() instead to align with external-store best practices.
| (onChange) => form.subscribe(() => onChange(), subscription), | |
| () => { | |
| const state = form.getState(); | |
| // Filter state based on subscription | |
| const result = {}; | |
| Object.keys(subscription).forEach((key) => { | |
| if (subscription[key]) { | |
| result[key] = state[key]; | |
| } | |
| }); | |
| return result; | |
| }, | |
| (onChange) => form.subscribeFormState(onChange, subscription), | |
| () => form.getFormSnapshot(), |
| (onChange) => form.registerField(fieldName, () => onChange(), subscription), | ||
| () => { | ||
| const fieldState = form.getFieldState(fieldName); | ||
| if (!fieldState) return undefined; | ||
|
|
||
| // Filter field state based on subscription | ||
| const result = {}; | ||
| Object.keys(subscription).forEach((key) => { | ||
| if (subscription[key]) { | ||
| result[key] = fieldState[key]; | ||
| } | ||
| }); | ||
| return result; | ||
| }, |
There was a problem hiding this comment.
Consider refactoring this hook to use form.subscribeFieldState and form.getFieldSnapshot so it matches the new API rather than form.registerField/getFieldState.
| (onChange) => form.registerField(fieldName, () => onChange(), subscription), | |
| () => { | |
| const fieldState = form.getFieldState(fieldName); | |
| if (!fieldState) return undefined; | |
| // Filter field state based on subscription | |
| const result = {}; | |
| Object.keys(subscription).forEach((key) => { | |
| if (subscription[key]) { | |
| result[key] = fieldState[key]; | |
| } | |
| }); | |
| return result; | |
| }, | |
| (onChange) => form.subscribeFieldState(fieldName, onChange, subscription), | |
| () => form.getFieldSnapshot(fieldName), |
No description provided.