Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/red-nails-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@radix-ui/react-use-controllable-state': patch
'@radix-ui/react-use-effect-event': patch
---

refactors the useEffectEvent hook implementation and updates useControllableState to use the new useEffectEvent hook for stable callback references.
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
import * as React from 'react';
import { useLayoutEffect } from '@radix-ui/react-use-layout-effect';

// Prevent bundlers from trying to optimize the import
const useInsertionEffect: typeof useLayoutEffect =
(React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;

type ChangeHandler<T> = (state: T) => void;
type SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;
Expand All @@ -21,7 +17,7 @@ export function useControllableState<T>({
onChange = () => {},
caller,
}: UseControllableStateParams<T>): [T, SetStateFn<T>] {
const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({
const [uncontrolledProp, setUncontrolledProp, stableOnChange] = useUncontrolledState({
defaultProp,
onChange,
});
Expand Down Expand Up @@ -53,13 +49,13 @@ export function useControllableState<T>({
if (isControlled) {
const value = isFunction(nextValue) ? nextValue(prop) : nextValue;
if (value !== prop) {
onChangeRef.current?.(value);
stableOnChange(value);
}
} else {
setUncontrolledProp(nextValue);
}
},
[isControlled, prop, setUncontrolledProp, onChangeRef],
[isControlled, prop, setUncontrolledProp, stableOnChange],
);

return [value, setValue];
Expand All @@ -71,24 +67,21 @@ function useUncontrolledState<T>({
}: Omit<UseControllableStateParams<T>, 'prop'>): [
Value: T,
setValue: React.Dispatch<React.SetStateAction<T>>,
OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,
OnChange: ChangeHandler<T>,
] {
const [value, setValue] = React.useState(defaultProp);
const prevValueRef = React.useRef(value);

const onChangeRef = React.useRef(onChange);
useInsertionEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
const stableOnChange = useEffectEvent(onChange ?? (() => {}));

React.useEffect(() => {
if (prevValueRef.current !== value) {
onChangeRef.current?.(value);
stableOnChange(value);
prevValueRef.current = value;
}
}, [value, prevValueRef]);
}, [value, prevValueRef, stableOnChange]);

return [value, setValue, onChangeRef];
return [value, setValue, stableOnChange];
}

function isFunction(value: unknown): value is (...args: any[]) => any {
Expand Down
12 changes: 0 additions & 12 deletions packages/react/use-effect-event/src/use-effect-event.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useLayoutEffect } from '@radix-ui/react-use-layout-effect';
import * as React from 'react';

type AnyFunction = (...args: any[]) => any;

// See https://github.com/webpack/webpack/issues/14814
const useReactEffectEvent = (React as any)[' useEffectEvent '.trim().toString()];
const useReactInsertionEffect = (React as any)[' useInsertionEffect '.trim().toString()];

/**
* Designed to approximate the behavior on `experimental_useEffectEvent` as best
Expand All @@ -20,16 +18,6 @@ export function useEffectEvent<T extends AnyFunction>(callback?: T): T {
const ref = React.useRef<AnyFunction | undefined>(() => {
throw new Error('Cannot call an event handler while rendering.');
});
// See https://github.com/webpack/webpack/issues/14814
if (typeof useReactInsertionEffect === 'function') {
useReactInsertionEffect(() => {
ref.current = callback;
});
} else {
useLayoutEffect(() => {
ref.current = callback;
});
}

// https://github.com/facebook/react/issues/19240
return React.useMemo(() => ((...args) => ref.current?.(...args)) as T, []);
Expand Down