Skip to content

Better support for useSyncExternalStore#498

Merged
erikras merged 4 commits intomainfrom
use-sync-external-store-support
May 25, 2025
Merged

Better support for useSyncExternalStore#498
erikras merged 4 commits intomainfrom
use-sync-external-store-support

Conversation

@erikras
Copy link
Member

@erikras erikras commented May 25, 2025

No description provided.

@erikras erikras requested a review from Copilot May 25, 2025 13:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/getFieldSnapshot and subscribeFormState/getFormSnapshot to the public API
  • Implements these new methods in createForm and 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 expect statements 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);

erikras and others added 2 commits May 25, 2025 15:12
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@erikras erikras requested a review from Copilot May 25, 2025 13:14
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FormApi types with subscribeFieldState, getFieldSnapshot, subscribeFormState, and getFormSnapshot.
  • Implements the new APIs in src/FinalForm.ts by 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.log calls with expect statements to verify fieldCallback is 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 a console.log.
console.log("After change, callback called:", fieldCallback.mock.calls.length);

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@erikras erikras requested a review from Copilot May 25, 2025 13:16
@erikras erikras merged commit e5cad94 into main May 25, 2025
3 checks passed
@erikras erikras deleted the use-sync-external-store-support branch May 25, 2025 13:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/getFieldSnapshot and subscribeFormState/getFormSnapshot in both types and createForm to 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);

Comment on lines +162 to +172
(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;
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
(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;

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +181
(onChange) => form.registerField(fieldName, () => onChange(), subscription),
() => {
const fieldState = form.getFieldState(fieldName);
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update this generic field hook to use form.subscribeFieldState(fieldName, onChange, subscription) and form.getFieldSnapshot(fieldName) instead of the legacy registerField/getFieldState.

Suggested change
(onChange) => form.registerField(fieldName, () => onChange(), subscription),
() => {
const fieldState = form.getFieldState(fieldName);
(onChange) => form.subscribeFieldState(fieldName, onChange, subscription),
() => {
const fieldState = form.getFieldSnapshot(fieldName);

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +146
(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;
},
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
(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(),

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +166
(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;
},
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider refactoring this hook to use form.subscribeFieldState and form.getFieldSnapshot so it matches the new API rather than form.registerField/getFieldState.

Suggested change
(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),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants