Skip to content

Commit 88d6662

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

File tree

6 files changed

+51
-37
lines changed

6 files changed

+51
-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: 11 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 {
@@ -18,13 +18,18 @@ const ExternallyChangedState: React.FC<Props> = ({ children, input, meta }) => {
1818
const hasInitializedRef = React.useRef(false)
1919
const initialRenderPhaseRef = React.useRef(true)
2020

21+
// The dependency array is intentionally omitted here because this effect
22+
// is designed to run on every render. It tracks value changes and field activity
23+
// to determine if changes were made externally (not by user interaction).
2124
React.useLayoutEffect(() => {
25+
// Initialize tracking on first run
2226
if (!hasInitializedRef.current) {
2327
hasInitializedRef.current = true
2428
previousValueRef.current = input.value
2529
return
2630
}
2731

32+
// Handle value changes
2833
if (input.value !== previousValueRef.current) {
2934
// Only consider it externally changed if:
3035
// 1. The field is not active AND
@@ -35,16 +40,19 @@ const ExternallyChangedState: React.FC<Props> = ({ children, input, meta }) => {
3540
if (initialRenderPhaseRef.current) {
3641
initialRenderPhaseRef.current = false
3742
// If this is the first change and field is not active, it's likely initialization
43+
// Skip setting externally changed to avoid false positives during form setup
3844
if (!meta.active) {
3945
previousValueRef.current = input.value
4046
return
4147
}
4248
}
4349

50+
// Update externally changed state and track the new value
4451
setExternallyChanged(wasExternallyChanged)
4552
previousValueRef.current = input.value
4653
} else if (meta.active && externallyChanged) {
47-
// Reset externally changed when field becomes active
54+
// Reset externally changed when field becomes active after external change
55+
// This allows users to "acknowledge" external changes by focusing the field
4856
setExternallyChanged(false)
4957
}
5058
})
@@ -60,7 +68,7 @@ const ExternallyChanged: React.FC<ExternallyChangedProps> = ({
6068
name,
6169
subscription: { active: true, value: true },
6270
allowNull: true,
63-
render: (props: any) =>
71+
render: (props: FieldRenderProps<any>) =>
6472
React.createElement(ExternallyChangedState, { ...props, children })
6573
})
6674

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: 18 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,35 @@ 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+
// The dependency array is intentionally omitted here because this effect
17+
// is designed to run on every render. It compares the current and previous
18+
// values of `input.value` and triggers the `children` callback if they differ.
19+
React.useLayoutEffect(() => {
20+
if (!hasInitializedRef.current) {
21+
hasInitializedRef.current = true
22+
previousValueRef.current = input.value
23+
return
2124
}
22-
}
2325

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)
26+
if (input.value !== previousValueRef.current) {
27+
children(input.value, previousValueRef.current)
28+
previousValueRef.current = input.value
3329
}
34-
}
30+
})
3531

36-
render() {
37-
return null
38-
}
32+
return null
3933
}
4034

4135
const OnChange: React.FC<OnChangeProps> = ({ name, children }) =>
4236
React.createElement(Field, {
4337
name,
4438
subscription: { value: true },
4539
allowNull: true,
46-
render: (props: any) =>
40+
render: (props: FieldRenderProps<any>) =>
4741
React.createElement(OnChangeState, { ...props, children })
4842
})
4943

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)