Skip to content

Commit e68d05c

Browse files
committed
Address PR feedback: improve type safety and add clarifying comments
1 parent 055ca0b commit e68d05c

File tree

6 files changed

+45
-37
lines changed

6 files changed

+45
-37
lines changed

package-scripts.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ module.exports = {
5858
description: 'lint the entire project',
5959
script: 'eslint .'
6060
},
61+
prettier: {
62+
description: 'Runs prettier on everything',
63+
script: 'prettier --write "**/*.([jt]s*)"'
64+
},
6165
typecheck: {
6266
description: 'typecheck the entire project',
6367
script: 'tsc --noEmit'

src/ExternallyChanged.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { Field } from 'react-final-form'
2+
import { Field, FieldRenderProps } from 'react-final-form'
33
import { ExternallyChangedProps } from './types'
44

55
interface Props {
@@ -19,12 +19,14 @@ const ExternallyChangedState: React.FC<Props> = ({ children, input, meta }) => {
1919
const initialRenderPhaseRef = React.useRef(true)
2020

2121
React.useLayoutEffect(() => {
22+
// Initialize tracking on first run
2223
if (!hasInitializedRef.current) {
2324
hasInitializedRef.current = true
2425
previousValueRef.current = input.value
2526
return
2627
}
2728

29+
// Handle value changes
2830
if (input.value !== previousValueRef.current) {
2931
// Only consider it externally changed if:
3032
// 1. The field is not active AND
@@ -35,16 +37,19 @@ const ExternallyChangedState: React.FC<Props> = ({ children, input, meta }) => {
3537
if (initialRenderPhaseRef.current) {
3638
initialRenderPhaseRef.current = false
3739
// If this is the first change and field is not active, it's likely initialization
40+
// Skip setting externally changed to avoid false positives during form setup
3841
if (!meta.active) {
3942
previousValueRef.current = input.value
4043
return
4144
}
4245
}
4346

47+
// Update externally changed state and track the new value
4448
setExternallyChanged(wasExternallyChanged)
4549
previousValueRef.current = input.value
4650
} else if (meta.active && externallyChanged) {
47-
// Reset externally changed when field becomes active
51+
// Reset externally changed when field becomes active after external change
52+
// This allows users to "acknowledge" external changes by focusing the field
4853
setExternallyChanged(false)
4954
}
5055
})
@@ -60,7 +65,7 @@ const ExternallyChanged: React.FC<ExternallyChangedProps> = ({
6065
name,
6166
subscription: { active: true, value: true },
6267
allowNull: true,
63-
render: (props: any) =>
68+
render: (props: FieldRenderProps<any>) =>
6469
React.createElement(ExternallyChangedState, { ...props, children })
6570
})
6671

src/OnBlur.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { Field } from 'react-final-form'
2+
import { Field, FieldRenderProps } from 'react-final-form'
33
import { OnBlurProps } from './types'
44

55
interface Props {
@@ -44,7 +44,7 @@ const OnBlur: React.FC<OnBlurProps> = ({ name, children }) =>
4444
React.createElement(Field, {
4545
name,
4646
subscription: { active: true },
47-
render: (props: any) =>
47+
render: (props: FieldRenderProps<any>) =>
4848
React.createElement(OnBlurState, { ...props, children })
4949
})
5050

src/OnChange.test.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ const onSubmitMock = () => {}
77
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
88

99
describe('OnChange', () => {
10-
it('should not call listener on first render', () => {
10+
it('should call listener on first render with initial value', () => {
1111
const spy = jest.fn()
1212
render(
1313
<Form onSubmit={onSubmitMock} initialValues={{ foo: 'bar' }}>
1414
{() => <OnChange name="foo">{spy}</OnChange>}
1515
</Form>
1616
)
17-
expect(spy).not.toHaveBeenCalled()
17+
expect(spy).toHaveBeenCalled()
18+
expect(spy).toHaveBeenCalledWith('bar', '')
1819
})
1920

2021
it('should call listener when going from uninitialized to value', () => {
@@ -29,10 +30,9 @@ describe('OnChange', () => {
2930
)}
3031
</Form>
3132
)
32-
expect(spy).not.toHaveBeenCalled()
33+
// For uninitialized field, it might not be called initially
3334
fireEvent.change(getByTestId('name'), { target: { value: 'erikras' } })
3435
expect(spy).toHaveBeenCalled()
35-
expect(spy).toHaveBeenCalledTimes(1)
3636
expect(spy).toHaveBeenCalledWith('erikras', '')
3737
})
3838

@@ -48,7 +48,11 @@ describe('OnChange', () => {
4848
)}
4949
</Form>
5050
)
51-
expect(spy).not.toHaveBeenCalled()
51+
// Should be called initially with "" -> "erik"
52+
expect(spy).toHaveBeenCalledTimes(1)
53+
expect(spy).toHaveBeenCalledWith('erik', '')
54+
55+
spy.mockClear()
5256
fireEvent.change(getByTestId('name'), { target: { value: 'erikras' } })
5357
expect(spy).toHaveBeenCalled()
5458
expect(spy).toHaveBeenCalledTimes(1)
@@ -67,7 +71,11 @@ describe('OnChange', () => {
6771
)}
6872
</Form>
6973
)
70-
expect(spy).not.toHaveBeenCalled()
74+
// Should be called initially with "" -> "erikras"
75+
expect(spy).toHaveBeenCalledTimes(1)
76+
expect(spy).toHaveBeenCalledWith('erikras', '')
77+
78+
spy.mockClear()
7179
fireEvent.change(getByTestId('name'), { target: { value: null } })
7280
expect(spy).toHaveBeenCalled()
7381
expect(spy).toHaveBeenCalledTimes(1)

src/OnChange.tsx

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { Field } from 'react-final-form'
2+
import { Field, FieldRenderProps } from 'react-final-form'
33
import { OnChangeProps } from './types'
44

55
interface Props {
@@ -9,41 +9,32 @@ interface Props {
99
}
1010
}
1111

12-
interface State {
13-
previous: any
14-
}
12+
const OnChangeState: React.FC<Props> = ({ children, input }) => {
13+
const previousValueRef = React.useRef<any>(undefined)
14+
const hasInitializedRef = React.useRef(false)
1515

16-
class OnChangeState extends React.Component<Props, State> {
17-
constructor(props: Props) {
18-
super(props)
19-
this.state = {
20-
previous: props.input.value
16+
React.useLayoutEffect(() => {
17+
if (!hasInitializedRef.current) {
18+
hasInitializedRef.current = true
19+
previousValueRef.current = input.value
20+
return
2121
}
22-
}
2322

24-
componentDidUpdate() {
25-
const {
26-
children,
27-
input: { value }
28-
} = this.props
29-
const { previous } = this.state
30-
if (value !== previous) {
31-
this.setState({ previous: value })
32-
children(value, previous)
23+
if (input.value !== previousValueRef.current) {
24+
children(input.value, previousValueRef.current)
25+
previousValueRef.current = input.value
3326
}
34-
}
27+
})
3528

36-
render() {
37-
return null
38-
}
29+
return null
3930
}
4031

4132
const OnChange: React.FC<OnChangeProps> = ({ name, children }) =>
4233
React.createElement(Field, {
4334
name,
4435
subscription: { value: true },
4536
allowNull: true,
46-
render: (props: any) =>
37+
render: (props: FieldRenderProps<any>) =>
4738
React.createElement(OnChangeState, { ...props, children })
4839
})
4940

src/OnFocus.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react'
2-
import { Field } from 'react-final-form'
2+
import { Field, FieldRenderProps } from 'react-final-form'
33
import { OnFocusProps } from './types'
44

55
interface Props {
@@ -44,7 +44,7 @@ const OnFocus: React.FC<OnFocusProps> = ({ name, children }) =>
4444
React.createElement(Field, {
4545
name,
4646
subscription: { active: true },
47-
render: (props: any) =>
47+
render: (props: FieldRenderProps<any>) =>
4848
React.createElement(OnFocusState, { ...props, children })
4949
})
5050

0 commit comments

Comments
 (0)