Skip to content

Commit dc7c982

Browse files
fix(react-form): default selector to identity in Subscribe component (#2065)
* fix(react-form): default selector to identity in Subscribe component When <form.Subscribe> is rendered without a selector prop, it crashes at runtime because useStore receives undefined as the selector: TypeError: selector is not a function The types already mark selector as optional, so the runtime behavior should match. This adds a default identity function ((state) => state) for the selector parameter in both LocalSubscribe implementations (useForm and useFieldGroup). Fixes #2063 * test: add coverage for Subscribe default selector * ci: apply automated fixes and generate docs --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 7ca9898 commit dc7c982

File tree

4 files changed

+105
-5
lines changed

4 files changed

+105
-5
lines changed

packages/react-form/src/useFieldGroup.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import type { LensFieldComponent } from './useField'
2323

2424
function LocalSubscribe({
2525
lens,
26-
selector,
26+
selector = (state) => state,
2727
children,
2828
}: PropsWithChildren<{
2929
lens: AnyFieldGroupApi
30-
selector: (state: FieldGroupState<any>) => FieldGroupState<any>
30+
selector?: (state: FieldGroupState<any>) => FieldGroupState<any>
3131
}>): ReturnType<FunctionComponent> {
3232
const data = useStore(lens.store, selector)
3333

packages/react-form/src/useForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ export type ReactFormExtendedApi<
139139

140140
function LocalSubscribe({
141141
form,
142-
selector,
142+
selector = (state) => state,
143143
children,
144144
}: PropsWithChildren<{
145145
form: AnyFormApi
146-
selector: (state: AnyFormState) => AnyFormState
146+
selector?: (state: AnyFormState) => AnyFormState
147147
}>): ReturnType<FunctionComponent> {
148148
const data = useStore(form.store, selector)
149149

packages/react-form/tests/createFormHook.test.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { render } from '@testing-library/react'
2+
import { render, waitFor } from '@testing-library/react'
33
import { formOptions } from '@tanstack/form-core'
44
import userEvent from '@testing-library/user-event'
55
import { createFormHook, createFormHookContexts, useStore } from '../src'
@@ -699,4 +699,63 @@ describe('createFormHook', () => {
699699
const button = getByText('Testing')
700700
expect(button).toBeInTheDocument()
701701
})
702+
703+
it('should render FieldGroup Subscribe without selector (default identity)', async () => {
704+
const formOpts = formOptions({
705+
defaultValues: {
706+
person: {
707+
firstName: 'FirstName',
708+
lastName: 'LastName',
709+
},
710+
},
711+
})
712+
713+
const ChildFormAsField = withFieldGroup({
714+
defaultValues: formOpts.defaultValues.person,
715+
render: ({ group }) => {
716+
return (
717+
<div>
718+
<group.Field
719+
name="lastName"
720+
children={(field) => (
721+
<label>
722+
Last Name:
723+
<input
724+
data-testid="lastName"
725+
value={field.state.value}
726+
onBlur={field.handleBlur}
727+
onChange={(e) => field.handleChange(e.target.value)}
728+
/>
729+
</label>
730+
)}
731+
/>
732+
<group.Subscribe
733+
children={(state) => (
734+
<span data-testid="state-lastName">
735+
{state.values.lastName}
736+
</span>
737+
)}
738+
/>
739+
</div>
740+
)
741+
},
742+
})
743+
744+
const Parent = () => {
745+
const form = useAppForm({
746+
...formOpts,
747+
})
748+
return <ChildFormAsField form={form} fields="person" />
749+
}
750+
751+
const { getByTestId } = render(<Parent />)
752+
const input = getByTestId('lastName')
753+
const stateLastName = getByTestId('state-lastName')
754+
755+
expect(stateLastName).toHaveTextContent('LastName')
756+
757+
await user.clear(input)
758+
await user.type(input, 'Updated')
759+
await waitFor(() => expect(stateLastName).toHaveTextContent('Updated'))
760+
})
702761
})

packages/react-form/tests/useForm.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,4 +1075,45 @@ describe('useForm', () => {
10751075
await user.click(getByText('Rerender'))
10761076
await findByText('Another')
10771077
})
1078+
1079+
it('should render Subscribe without selector (default identity)', async () => {
1080+
function Comp() {
1081+
const form = useForm({
1082+
defaultValues: {
1083+
name: 'test',
1084+
},
1085+
})
1086+
1087+
return (
1088+
<>
1089+
<form.Field
1090+
name="name"
1091+
children={(field) => (
1092+
<input
1093+
data-testid="input"
1094+
value={field.state.value}
1095+
onChange={(e) => field.handleChange(e.target.value)}
1096+
/>
1097+
)}
1098+
/>
1099+
1100+
<form.Subscribe
1101+
children={(state) => (
1102+
<span data-testid="state-value">{state.values.name}</span>
1103+
)}
1104+
/>
1105+
</>
1106+
)
1107+
}
1108+
1109+
const { getByTestId } = render(<Comp />)
1110+
const input = getByTestId('input')
1111+
const stateValue = getByTestId('state-value')
1112+
1113+
expect(stateValue).toHaveTextContent('test')
1114+
1115+
await user.clear(input)
1116+
await user.type(input, 'updated')
1117+
await waitFor(() => expect(stateValue).toHaveTextContent('updated'))
1118+
})
10781119
})

0 commit comments

Comments
 (0)