From 9ec801278f64da02f0611dc7881eb8a539033ebb Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 3 Feb 2023 16:00:51 -0800 Subject: [PATCH 1/6] Fix React Strictmode for sliders and useControlledState --- packages/@react-aria/slider/src/useSlider.ts | 6 ++++-- .../@react-aria/slider/src/useSliderThumb.ts | 4 +++- .../slider/test/RangeSlider.test.tsx | 13 ++++++++----- .../slider/test/Slider.test.tsx | 13 ++++++++----- .../slider/src/useSliderState.ts | 8 +++++--- .../utils/src/useControlledState.ts | 18 +++++++++++------- .../utils/test/useControlledState.test.js | 8 ++++++-- 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index 668d40822d2..b6a86d57220 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -14,7 +14,7 @@ import {AriaSliderProps} from '@react-types/slider'; import {clamp, mergeProps, useGlobalListeners} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {getSliderThumbId, sliderIds} from './utils'; -import React, {LabelHTMLAttributes, OutputHTMLAttributes, RefObject, useRef} from 'react'; +import React, {LabelHTMLAttributes, OutputHTMLAttributes, RefObject, useEffect, useRef} from 'react'; import {setInteractionModality, useMove} from '@react-aria/interactions'; import {SliderState} from '@react-stately/slider'; import {useLabel} from '@react-aria/label'; @@ -67,7 +67,9 @@ export function useSlider( const realTimeTrackDraggingIndex = useRef(null); const stateRef = useRef(null); - stateRef.current = state; + useEffect(() => { + stateRef.current = state; + }); const reverseX = direction === 'rtl'; const currentPosition = useRef(null); const {moveProps} = useMove({ diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 8543f4ee6b7..0e3c9532792 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -83,7 +83,9 @@ export function useSliderThumb( }, [isFocused, focusInput]); const stateRef = useRef(null); - stateRef.current = state; + useEffect(() => { + stateRef.current = state; + }); let reverseX = direction === 'rtl'; let currentPosition = useRef(null); diff --git a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx index c2952bb2363..7ff0f2bb68c 100644 --- a/packages/@react-spectrum/slider/test/RangeSlider.test.tsx +++ b/packages/@react-spectrum/slider/test/RangeSlider.test.tsx @@ -14,7 +14,7 @@ import {fireEvent, render} from '@react-spectrum/test-utils'; import {press, testKeypresses} from './utils'; import {Provider} from '@adobe/react-spectrum'; import {RangeSlider} from '../'; -import React, {useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; @@ -126,11 +126,14 @@ describe('RangeSlider', function () { }); it('can be controlled', function () { - let renders = []; + let setValues = []; function Test() { - let [value, setValue] = useState({start: 20, end: 40}); - renders.push(value); + let [value, _setValue] = useState({start: 20, end: 40}); + let setValue = useCallback((val) => { + setValues.push(val); + _setValue(val); + }, [_setValue]); return (); } @@ -154,7 +157,7 @@ describe('RangeSlider', function () { expect(sliderRight).toHaveAttribute('aria-valuetext', '50'); expect(output).toHaveTextContent('30 – 50'); - expect(renders).toStrictEqual([{start: 20, end: 40}, {start: 30, end: 40}, {start: 30, end: 50}]); + expect(setValues).toStrictEqual([{start: 30, end: 40}, {start: 30, end: 50}]); }); it('supports a custom valueLabel', function () { diff --git a/packages/@react-spectrum/slider/test/Slider.test.tsx b/packages/@react-spectrum/slider/test/Slider.test.tsx index c28974c8920..4acd318497e 100644 --- a/packages/@react-spectrum/slider/test/Slider.test.tsx +++ b/packages/@react-spectrum/slider/test/Slider.test.tsx @@ -13,7 +13,7 @@ import {act, fireEvent, installMouseEvent, render} from '@react-spectrum/test-utils'; import {press, testKeypresses} from './utils'; import {Provider} from '@adobe/react-spectrum'; -import React, {useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {Slider} from '../'; import {theme} from '@react-spectrum/theme-default'; import userEvent from '@testing-library/user-event'; @@ -119,11 +119,14 @@ describe('Slider', function () { }); it('can be controlled', function () { - let renders = []; + let setValues = []; function Test() { - let [value, setValue] = useState(50); - renders.push(value); + let [value, _setValue] = useState(50); + let setValue = useCallback((val) => { + setValues.push(val); + _setValue(val); + }, [_setValue]); return (); } @@ -141,7 +144,7 @@ describe('Slider', function () { expect(slider).toHaveAttribute('aria-valuetext', '55'); expect(output).toHaveTextContent('55'); - expect(renders).toStrictEqual([50, 55]); + expect(setValues).toStrictEqual([55]); }); it('supports a custom getValueLabel', function () { diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index 2132000bd45..1e40dff4bf1 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -14,7 +14,7 @@ import {clamp, snapValueToStep} from '@react-aria/utils'; import {Orientation} from '@react-types/shared'; import {SliderProps} from '@react-types/slider'; import {useControlledState} from '@react-stately/utils'; -import {useMemo, useRef, useState} from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; export interface SliderState { /** @@ -192,9 +192,11 @@ export function useSliderState(props: SliderStateOp const [focusedIndex, setFocusedIndex] = useState(undefined); const valuesRef = useRef(null); - valuesRef.current = values; const isDraggingsRef = useRef(null); - isDraggingsRef.current = isDraggings; + useEffect(() => { + valuesRef.current = values; + isDraggingsRef.current = isDraggings; + }); function getValuePercent(value: number) { return (value - minValue) / (maxValue - minValue); diff --git a/packages/@react-stately/utils/src/useControlledState.ts b/packages/@react-stately/utils/src/useControlledState.ts index 2e394844b0a..5a0832e7833 100644 --- a/packages/@react-stately/utils/src/useControlledState.ts +++ b/packages/@react-stately/utils/src/useControlledState.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {useCallback, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; export function useControlledState( value: T, @@ -26,8 +26,9 @@ export function useControlledState( if (wasControlled !== isControlled) { console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`); } - - ref.current = isControlled; + useEffect(() => { + ref.current = isControlled; + }); let setValue = useCallback((value, ...args) => { let onChangeCaller = (value, ...onChangeArgs) => { @@ -65,10 +66,13 @@ export function useControlledState( } }, [isControlled, onChange]); - // If a controlled component's value prop changes, we need to update stateRef - if (isControlled) { - stateRef.current = value; - } else { + useEffect(() => { + // If a controlled component's value prop changes, we need to update stateRef + if (isControlled) { + stateRef.current = value; + } + }); + if (!isControlled) { value = stateValue; } diff --git a/packages/@react-stately/utils/test/useControlledState.test.js b/packages/@react-stately/utils/test/useControlledState.test.js index 1a6875c0528..e904a397c9b 100644 --- a/packages/@react-stately/utils/test/useControlledState.test.js +++ b/packages/@react-stately/utils/test/useControlledState.test.js @@ -99,12 +99,16 @@ describe('useControlledState tests', function () { let {getByRole, getByTestId} = render(); let button = getByRole('button'); getByTestId('5'); - expect(renderSpy).toBeCalledTimes(1); + if (!process.env.STRICT_MODE) { + expect(renderSpy).toBeCalledTimes(1); + } actDOM(() => userEvent.click(button) ); getByTestId('6'); - expect(renderSpy).toBeCalledTimes(2); + if (!process.env.STRICT_MODE) { + expect(renderSpy).toBeCalledTimes(2); + } }); it('can handle controlled setValue behavior', () => { From a61033a7923974ece70c1ada94b78540d2bff68c Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 3 Feb 2023 17:24:01 -0800 Subject: [PATCH 2/6] Fix useControlledState more --- .../utils/src/useControlledState.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/@react-stately/utils/src/useControlledState.ts b/packages/@react-stately/utils/src/useControlledState.ts index 5a0832e7833..33ef27c807c 100644 --- a/packages/@react-stately/utils/src/useControlledState.ts +++ b/packages/@react-stately/utils/src/useControlledState.ts @@ -18,31 +18,33 @@ export function useControlledState( onChange: (value: T, ...args: any[]) => void ): [T, (value: T, ...args: any[]) => void] { let [stateValue, setStateValue] = useState(value || defaultValue); - let ref = useRef(value !== undefined); - let wasControlled = ref.current; let isControlled = value !== undefined; // Internal state reference for useCallback let stateRef = useRef(stateValue); - if (wasControlled !== isControlled) { - console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`); - } + + // detect if a component is switching from controlled to uncontrolled or vice versa, warn if so + let isControlledRef = useRef(isControlled); useEffect(() => { - ref.current = isControlled; - }); + let wasControlled = isControlledRef.current; + if (isControlled !== wasControlled) { + console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`); + } + isControlledRef.current = isControlled; + }, [value, isControlled]); - let setValue = useCallback((value, ...args) => { - let onChangeCaller = (value, ...onChangeArgs) => { + let setValue = useCallback((newValue, ...args) => { + let onChangeCaller = (updateValue, ...onChangeArgs) => { if (onChange) { - if (!Object.is(stateRef.current, value)) { - onChange(value, ...onChangeArgs); + if (!Object.is(stateRef.current, updateValue)) { + onChange(updateValue, ...onChangeArgs); } } if (!isControlled) { - stateRef.current = value; + stateRef.current = updateValue; } }; - if (typeof value === 'function') { + if (typeof newValue === 'function') { console.warn('We can not support a function callback. See Github Issues for details https://github.com/adobe/react-spectrum/issues/2320'); // this supports functional updates https://reactjs.org/docs/hooks-reference.html#functional-updates // when someone using useControlledState calls setControlledState(myFunc) @@ -50,7 +52,7 @@ export function useControlledState( // if we're in an uncontrolled state, then we also return the value of myFunc which to setState looks as though it was just called with myFunc from the beginning // otherwise we just return the controlled value, which won't cause a rerender because React knows to bail out when the value is the same let updateFunction = (oldValue, ...functionArgs) => { - let interceptedValue = value(isControlled ? stateRef.current : oldValue, ...functionArgs); + let interceptedValue = newValue(isControlled ? stateRef.current : oldValue, ...functionArgs); onChangeCaller(interceptedValue, ...args); if (!isControlled) { return interceptedValue; @@ -60,9 +62,9 @@ export function useControlledState( setStateValue(updateFunction); } else { if (!isControlled) { - setStateValue(value); + setStateValue(newValue); } - onChangeCaller(value, ...args); + onChangeCaller(newValue, ...args); } }, [isControlled, onChange]); From a4e9e91629349d2c2865e3dec991d4fabeee952d Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Fri, 3 Feb 2023 19:17:04 -0800 Subject: [PATCH 3/6] fix lint --- packages/@react-stately/slider/src/useSliderState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-stately/slider/src/useSliderState.ts b/packages/@react-stately/slider/src/useSliderState.ts index 1e40dff4bf1..f4533439d60 100644 --- a/packages/@react-stately/slider/src/useSliderState.ts +++ b/packages/@react-stately/slider/src/useSliderState.ts @@ -14,7 +14,7 @@ import {clamp, snapValueToStep} from '@react-aria/utils'; import {Orientation} from '@react-types/shared'; import {SliderProps} from '@react-types/slider'; import {useControlledState} from '@react-stately/utils'; -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useEffect, useMemo, useRef, useState} from 'react'; export interface SliderState { /** From d56524c7a37690c5a74dfc2ef23791ab1912e162 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 9 Feb 2023 10:22:33 -0800 Subject: [PATCH 4/6] update to synchronous effect --- packages/@react-aria/slider/src/useSlider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index b6a86d57220..d54df472379 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -11,10 +11,10 @@ */ import {AriaSliderProps} from '@react-types/slider'; -import {clamp, mergeProps, useGlobalListeners} from '@react-aria/utils'; +import {clamp, mergeProps, useGlobalListeners, useLayoutEffect} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {getSliderThumbId, sliderIds} from './utils'; -import React, {LabelHTMLAttributes, OutputHTMLAttributes, RefObject, useEffect, useRef} from 'react'; +import React, {LabelHTMLAttributes, OutputHTMLAttributes, RefObject, useRef} from 'react'; import {setInteractionModality, useMove} from '@react-aria/interactions'; import {SliderState} from '@react-stately/slider'; import {useLabel} from '@react-aria/label'; @@ -67,7 +67,7 @@ export function useSlider( const realTimeTrackDraggingIndex = useRef(null); const stateRef = useRef(null); - useEffect(() => { + useLayoutEffect(() => { stateRef.current = state; }); const reverseX = direction === 'rtl'; From 9a345bdcb55fd54e8d1a6e2e287fed3879576d1d Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Wed, 15 Feb 2023 14:37:12 -0800 Subject: [PATCH 5/6] move to layouteffect --- packages/@react-aria/slider/src/useSliderThumb.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/@react-aria/slider/src/useSliderThumb.ts b/packages/@react-aria/slider/src/useSliderThumb.ts index 0e3c9532792..c16e9aa86f4 100644 --- a/packages/@react-aria/slider/src/useSliderThumb.ts +++ b/packages/@react-aria/slider/src/useSliderThumb.ts @@ -1,8 +1,16 @@ import {AriaSliderThumbProps} from '@react-types/slider'; -import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners} from '@react-aria/utils'; +import {clamp, focusWithoutScrolling, mergeProps, useGlobalListeners, useLayoutEffect} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {getSliderThumbId, sliderIds} from './utils'; -import React, {ChangeEvent, InputHTMLAttributes, LabelHTMLAttributes, RefObject, useCallback, useEffect, useRef} from 'react'; +import React, { + ChangeEvent, + InputHTMLAttributes, + LabelHTMLAttributes, + RefObject, + useCallback, + useEffect, + useRef +} from 'react'; import {SliderState} from '@react-stately/slider'; import {useFocusable} from '@react-aria/focus'; import {useKeyboard, useMove} from '@react-aria/interactions'; @@ -83,7 +91,7 @@ export function useSliderThumb( }, [isFocused, focusInput]); const stateRef = useRef(null); - useEffect(() => { + useLayoutEffect(() => { stateRef.current = state; }); let reverseX = direction === 'rtl'; From e98cffc5d4fbacf37779e27e474c5093d2e36d63 Mon Sep 17 00:00:00 2001 From: Rob Snow Date: Thu, 13 Apr 2023 18:02:43 -0700 Subject: [PATCH 6/6] revert controlled state and move to useEffectEvent --- packages/@react-aria/slider/src/useSlider.ts | 62 +++++++++---------- .../utils/src/useControlledState.ts | 46 ++++++-------- 2 files changed, 51 insertions(+), 57 deletions(-) diff --git a/packages/@react-aria/slider/src/useSlider.ts b/packages/@react-aria/slider/src/useSlider.ts index d54df472379..4556a5af3d7 100644 --- a/packages/@react-aria/slider/src/useSlider.ts +++ b/packages/@react-aria/slider/src/useSlider.ts @@ -11,7 +11,7 @@ */ import {AriaSliderProps} from '@react-types/slider'; -import {clamp, mergeProps, useGlobalListeners, useLayoutEffect} from '@react-aria/utils'; +import {clamp, mergeProps, useEffectEvent, useGlobalListeners} from '@react-aria/utils'; import {DOMAttributes} from '@react-types/shared'; import {getSliderThumbId, sliderIds} from './utils'; import React, {LabelHTMLAttributes, OutputHTMLAttributes, RefObject, useRef} from 'react'; @@ -61,48 +61,48 @@ export function useSlider( let {addGlobalListener, removeGlobalListener} = useGlobalListeners(); // When the user clicks or drags the track, we want the motion to set and drag the - // closest thumb. Hence we also need to install useMove() on the track element. + // closest thumb. Hence, we also need to install useMove() on the track element. // Here, we keep track of which index is the "closest" to the drag start point. // It is set onMouseDown/onTouchDown; see trackProps below. const realTimeTrackDraggingIndex = useRef(null); - const stateRef = useRef(null); - useLayoutEffect(() => { - stateRef.current = state; - }); + const reverseX = direction === 'rtl'; const currentPosition = useRef(null); - const {moveProps} = useMove({ - onMoveStart() { - currentPosition.current = null; - }, - onMove({deltaX, deltaY}) { - let {height, width} = trackRef.current.getBoundingClientRect(); - let size = isVertical ? height : width; + let onMoveStart = useEffectEvent(() => { + currentPosition.current = null; + }); + let onMove = useEffectEvent(({deltaX, deltaY}) => { + let {height, width} = trackRef.current.getBoundingClientRect(); + let size = isVertical ? height : width; - if (currentPosition.current == null) { - currentPosition.current = stateRef.current.getThumbPercent(realTimeTrackDraggingIndex.current) * size; - } + if (currentPosition.current == null) { + currentPosition.current = state.getThumbPercent(realTimeTrackDraggingIndex.current) * size; + } - let delta = isVertical ? deltaY : deltaX; - if (isVertical || reverseX) { - delta = -delta; - } + let delta = isVertical ? deltaY : deltaX; + if (isVertical || reverseX) { + delta = -delta; + } - currentPosition.current += delta; + currentPosition.current += delta; - if (realTimeTrackDraggingIndex.current != null && trackRef.current) { - const percent = clamp(currentPosition.current / size, 0, 1); - stateRef.current.setThumbPercent(realTimeTrackDraggingIndex.current, percent); - } - }, - onMoveEnd() { - if (realTimeTrackDraggingIndex.current != null) { - stateRef.current.setThumbDragging(realTimeTrackDraggingIndex.current, false); - realTimeTrackDraggingIndex.current = null; - } + if (realTimeTrackDraggingIndex.current != null && trackRef.current) { + const percent = clamp(currentPosition.current / size, 0, 1); + state.setThumbPercent(realTimeTrackDraggingIndex.current, percent); } }); + let onMoveEnd = useEffectEvent(() => { + if (realTimeTrackDraggingIndex.current != null) { + state.setThumbDragging(realTimeTrackDraggingIndex.current, false); + realTimeTrackDraggingIndex.current = null; + } + }); + const {moveProps} = useMove({ + onMoveStart, + onMove, + onMoveEnd + }); let currentPointer = useRef(undefined); let onDownTrack = (e: React.UIEvent, id: number, clientX: number, clientY: number) => { diff --git a/packages/@react-stately/utils/src/useControlledState.ts b/packages/@react-stately/utils/src/useControlledState.ts index 33ef27c807c..2e394844b0a 100644 --- a/packages/@react-stately/utils/src/useControlledState.ts +++ b/packages/@react-stately/utils/src/useControlledState.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {useCallback, useEffect, useRef, useState} from 'react'; +import {useCallback, useRef, useState} from 'react'; export function useControlledState( value: T, @@ -18,33 +18,30 @@ export function useControlledState( onChange: (value: T, ...args: any[]) => void ): [T, (value: T, ...args: any[]) => void] { let [stateValue, setStateValue] = useState(value || defaultValue); + let ref = useRef(value !== undefined); + let wasControlled = ref.current; let isControlled = value !== undefined; // Internal state reference for useCallback let stateRef = useRef(stateValue); + if (wasControlled !== isControlled) { + console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`); + } - // detect if a component is switching from controlled to uncontrolled or vice versa, warn if so - let isControlledRef = useRef(isControlled); - useEffect(() => { - let wasControlled = isControlledRef.current; - if (isControlled !== wasControlled) { - console.warn(`WARN: A component changed from ${wasControlled ? 'controlled' : 'uncontrolled'} to ${isControlled ? 'controlled' : 'uncontrolled'}.`); - } - isControlledRef.current = isControlled; - }, [value, isControlled]); + ref.current = isControlled; - let setValue = useCallback((newValue, ...args) => { - let onChangeCaller = (updateValue, ...onChangeArgs) => { + let setValue = useCallback((value, ...args) => { + let onChangeCaller = (value, ...onChangeArgs) => { if (onChange) { - if (!Object.is(stateRef.current, updateValue)) { - onChange(updateValue, ...onChangeArgs); + if (!Object.is(stateRef.current, value)) { + onChange(value, ...onChangeArgs); } } if (!isControlled) { - stateRef.current = updateValue; + stateRef.current = value; } }; - if (typeof newValue === 'function') { + if (typeof value === 'function') { console.warn('We can not support a function callback. See Github Issues for details https://github.com/adobe/react-spectrum/issues/2320'); // this supports functional updates https://reactjs.org/docs/hooks-reference.html#functional-updates // when someone using useControlledState calls setControlledState(myFunc) @@ -52,7 +49,7 @@ export function useControlledState( // if we're in an uncontrolled state, then we also return the value of myFunc which to setState looks as though it was just called with myFunc from the beginning // otherwise we just return the controlled value, which won't cause a rerender because React knows to bail out when the value is the same let updateFunction = (oldValue, ...functionArgs) => { - let interceptedValue = newValue(isControlled ? stateRef.current : oldValue, ...functionArgs); + let interceptedValue = value(isControlled ? stateRef.current : oldValue, ...functionArgs); onChangeCaller(interceptedValue, ...args); if (!isControlled) { return interceptedValue; @@ -62,19 +59,16 @@ export function useControlledState( setStateValue(updateFunction); } else { if (!isControlled) { - setStateValue(newValue); + setStateValue(value); } - onChangeCaller(newValue, ...args); + onChangeCaller(value, ...args); } }, [isControlled, onChange]); - useEffect(() => { - // If a controlled component's value prop changes, we need to update stateRef - if (isControlled) { - stateRef.current = value; - } - }); - if (!isControlled) { + // If a controlled component's value prop changes, we need to update stateRef + if (isControlled) { + stateRef.current = value; + } else { value = stateValue; }